In the previous part of this series, we got the player's ship moving, got the invaders moving, and detected when a player bullet had hit an invader. In this final part of the series, we will get the invaders attacking the player, handle levels, and add the ability for the player to die.
1. Firing Bullets
Every so often one of the invaders fires a bullet. We'll use a timer to accomplish this. Add the following code to gamelevel.lua.
function fireInvaderBullet() if(#invadersWhoCanFire >0) then local randomIndex = math.random(#invadersWhoCanFire) local randomInvader = invadersWhoCanFire[randomIndex] local tempInvaderBullet = display.newImage("laser.png", randomInvader.x , randomInvader.y + invaderSize/2) tempInvaderBullet.name = "invaderBullet" scene.view:insert(tempInvaderBullet) physics.addBody(tempInvaderBullet, "dynamic" ) tempInvaderBullet.gravityScale = 0 tempInvaderBullet.isBullet = true tempInvaderBullet.isSensor = true tempInvaderBullet:setLinearVelocity( 0,400) table.insert(invaderBullets, tempInvaderBullet) else levelComplete() end end
In this function, we first check if the invadersWhoCanFire
table has at least one element in it. If that's the case, then we execute the code in the if statement. Otherwise it means the level is over and we invoke the levelComplete
function.
There will always be at least one invader who can fire a bullet until you kill the last invader, at which point the invadersWhoCanFire
table will be empty.
Inside the if statement, we generate a random number randomIndex
depending on how many items are in the invadersWhoCanFire
table. We then choose that item, randomInvader
, from the invadersWhoCanFire
table.
We create a bullet image, give it a name
property so we can identify it later, insert it into the scene, and set the same properties as we did on the player's bullet. Finally, we insert the bullet into the invaderBullets
table so we can reference it later.
We now need to set up the timer. Add the following to the scene:show
method.
function scene:show(event) if ( phase == "did" ) then --SNIP-- Runtime:addEventListener( "collision", onCollision ) invaderFireTimer = timer.performWithDelay(1500, fireInvaderBullet,-1) end end
Every 1500 milliseconds fireInvaderBullet
is invoked. Note that the last parameter we pass in is -1
, which means the timer repeats forever. Whenever you create a timer that repeats forever, you should eventually cancel it. We do this in thescene:hide
function as shown below.
function scene:hide(event) if ( phase == "will" ) then --SNIP-- Runtime:removeEventListener( "collision", onCollision ) timer.cancel(invaderFireTimer) end end
2. Removing Bullets
Like the player's bullets, the invaders' bullets will move off-screen and keep moving, taking up valuable memory. To remedy this, we remove them just like we did with the player's bullets.
function checkInvaderBulletsOutOfBounds() if (#invaderBullets > 0) then for i=#invaderBullets,1,-1 do if(invaderBullets[i].y > display.contentHeight) then invaderBullets[i]:removeSelf() invaderBullets[i] = nil table.remove(invaderBullets,i) end end end end
This code is very similar to checking if the player's bullets are out of bounds so I won't discuss its implementation in detail.
3. Detecting a Hit
Step 1: Collision Detection
The next step is to detect whether an invader's bullet has hit the player. Add the following code to the onCollision
function.
function onCollision(event) if ( event.phase == "began" ) then --SNIP-- if(event.object1.name == "player" and event.object2.name == "invaderBullet") then table.remove(invaderBullets,table.indexOf(invaderBullets,event.object2)) event.object2:removeSelf() event.object2 = nil if(playerIsInvincible == false) then killPlayer() end return end if(event.object1.name == "invaderBullet" and event.object2.name == "player") then table.remove(invaderBullets,table.indexOf(invaderBullets,event.object1)) event.object1:removeSelf() event.object1 = nil if(playerIsInvincible == false) then killPlayer() end return end end end
Like before, we do not know what object event.object1
and event.object2
will be so we use two if statements to check both situations. We remove the invader's bullet from the invaderBullets
table, remove it from the display, and set it to nil
. If the player isn't invincible, we kill the it.
Step 2: Killing the Player
When we kill the player, we give him a short time of invincibility. This gives the user time to regain focus on the game. If the numberOfLives
variable is equal to 0
, we know the game is over and transition to the start scene where the user can begin a new game.
function killPlayer() numberOfLives = numberOfLives- 1; if(numberOfLives <= 0) then gameData.invaderNum = 1 composer.gotoScene("start") else playerIsInvincible = true spawnNewPlayer() end end
Step 3: Spawning a New Player
The spawnNewPlayer
function makes the player fade in and out for a few seconds. It's a nice effect to let the user know that the ship is temporarily invincible.
function spawnNewPlayer() local numberOfTimesToFadePlayer = 5 local numberOfTimesPlayerHasFaded = 0 local function fadePlayer() player.alpha = 0; transition.to( player, {time=400, alpha=1, }) numberOfTimesPlayerHasFaded = numberOfTimesPlayerHasFaded + 1 if(numberOfTimesPlayerHasFaded == numberOfTimesToFadePlayer) then playerIsInvincible = false end end fadePlayer() timer.performWithDelay(400, fadePlayer,numberOfTimesToFadePlayer) end
We use a local function, fadePlayer
, that uses the transition library to modify the alpha
value of the player
. We keep track of how many times the player
has faded in and out, and set the player's invincibility to false
once we reach the numberOfTimesToFadePlayer
. We use a timer to call the fadePlayer
function for however many times numberOfTimesToFadePlayer
is equal to.
Run the game to test this out. The player
should die when an invader's bullet hits the ship. If three bullets hit the ship, you should be taken to the start scene where you can start a new game.
To make this easier to test, comment out the call to moveInvaders
in the gameLoop
function as shown below.
function gameLoop() checkPlayerBulletsOutOfBounds() --moveInvaders() checkInvaderBulletsOutOfBounds() end
4. Completing a Level
If you've managed to kill every invader, the game would have called the levelComplete
function, which doesn't exist yet. Let fix that. Add the following code block.
function levelComplete() gameData.invaderNum = gameData.invaderNum + 1 if(gameData.invaderNum <= gameData.maxLevels) then composer.gotoScene("gameover") else gameData.invaderNum = 1 composer.gotoScene("start") end end
We increment gameData.invaderNum
and, if it is less than gameData.maxLevels
, we transition to the gameover scene. Otherwise, the player has completed every level and we reset gameData.invaderNum
to 1. We transition to the start scene where the player can begin a new game.
An easy way to test this is by commenting out the call to moveInvaders
in the gameLoop
function and use the buttons to move the ship. If that's still too hard, then you can also comment out the two calls to killPlayer
in the onCollision
method.
5. Game Over
Add the following code to gameover.lua to implement the game over scene.
local composer = require("composer") local scene = composer.newScene() local starFieldGenerator = require("starfieldgenerator") local pulsatingText = require("pulsatingtext") local nextLevelButton local starGenerator function scene:create( event ) local group = self.view starGenerator = starFieldGenerator.new(200,group,5) local invadersText = pulsatingText.new("LEVEL COMPLETE", display.contentCenterX, display.contentCenterY-200,"Conquest", 20,group ) invadersText:setColor( 1, 1, 1 ) invadersText:pulsate() nextLevelButton = display.newImage("next_level_btn.png",display.contentCenterX, display.contentCenterY) group:insert(nextLevelButton) end function scene:show( event ) local phase = event.phase composer.removeScene("gamelevel" ) if ( phase == "did" ) then nextLevelButton:addEventListener("tap",startNewGame) Runtime:addEventListener ( "enterFrame", starGenerator) end end function scene:hide(event ) local phase = event.phase if ( phase == "will" ) then Runtime:removeEventListener("enterFrame", starGenerator) nextLevelButton:removeEventListener("tap",startNewGame) end end function startNewGame() composer.gotoScene("gamelevel") end scene:addEventListener( "create", scene ) scene:addEventListener( "show", scene ) scene:addEventListener( "hide", scene ) return scene
This code is very similar to the start scene so you should be familiar with it by now.
6. Colliding with an Invader
The last collision check we need to perform is a collision between the player and on of the invaders. Add the following code block to the onCollision
method we saw earlier.
function onCollision(event) --SNIP-- if ( event.phase == "began" ) then --SNIP-- if(event.object1.name == "player" and event.object2.name == "invader") then numberOfLives = 0 killPlayer() end if(event.object1.name == "invader" and event.object2.name == "player") then numberOfLives = 0 killPlayer() end end
As usual, we need to check both collision situations. We set the numberOfLives
to 0 and call killPlayer
. By setting numberOfLives
to 0 and invoking killPlayer
, the game is over and the game transitions to the start scene.
7. More Features
This completes our game, but I suggest you try to expand the game with a few additional features. For example, you could display the player's lives in a HUD.
I have also included a UFO graphic in the source files. You could try make a UFO randomly appear and if the player hits it with a bullet give them an extra life.
If you need help with these concepts, then check out my Plane Fighting Game series on Tuts+.
Conclusion
If you've followed this series, then you should now have a fully functional game similar to the original Space Invaders. Expand on it and make it your own. I hope you found this tutorial helpful and have learned some new techniques. Thank you for for reading.