In the first part of this series, we set up some defaults for the game and laid the foundation for transitioning between scenes. In this part, we'll begin implementing the game's gameplay.
1. A Word about Metatables
The Lua programming language does not have a class system built in. However, by using Lua's metatable construct we can emulate a class system. There is a good example on the Corona website showing how to implement this.
An important thing to note is that Corona's Display
objects cannot be set as the metatable. This has to do with how the underlying C language interfaces with them. A simple way to get around this is to set the Display
object as a key on a new table and then put that table as the metatable. This is the approach that we'll take in this tutorial.
If you read the above article on the Corona website, you will have noticed that the __Index
metamethod was being used on the metatable. The way the __Index
metamethod works, is that when you try to access an absent field in a table, it triggers the interpreter to look for an __Index
metamethod. If the __Index
is there, it will look for the field and provide the result, otherwise it will result in nil
.
2. Implementing PulsatingText
Class
The game has text that continuously grows and shrinks, creating a pulsating text effect. We will create this functionality as a module so we can use it throughout the project. Also, by having it as a module, we can use it in any project that would require this type of functionality.
Add the following to the pulsatingtext.lua file you created in the first part of this tutorial. Make sure this code and all code from here on is placed above where you are returning the scene
object.
local pulsatingText = {} local pulsatingText_mt = {__index = pulsatingText} function pulsatingText.new(theText,positionX,positionY,theFont,theFontSize,theGroup) local theTextField = display.newText(theText,positionX,positionY,theFont,theFontSize) local newPulsatingText = { theTextField = theTextField} theGroup:insert(theTextField) return setmetatable(newPulsatingText,pulsatingText_mt) end function pulsatingText:setColor(r,b,g) self.theTextField:setFillColor(r,g,b) end function pulsatingText:pulsate() transition.to( self.theTextField, { xScale=4.0, yScale=4.0, time=1500, iterations = -1} ) end return pulsatingText
We create the main table pulsatingText
and the table to be used as the metatable pulsatingText_mt
. In the new
method, we create the TextField
object and add it to the table newPulsatingText
that will be set as the metatable. We then add the TextField
object to the group
that was passed in through the parameter, which will be the scene's group in which we instantiate an instance of PulsatingText
.
It's important to make sure that we add it to the scene's group so it will be removed when the scene is removed. Finally, we set the metatable.
We have two methods that access the TextField
object and perform operations on its properties. One sets the color by using the setFillColor
method and takes as parameters the R, G, and B colors as a number from 0 to 1. The other uses the Transition library to make the text grow and shrink. It enlarges the text by using the xScale
and yScale
properties. Setting the iterations
property to -1 makes the action repeat forever.
3. Using the PulsatingText
Class
Open start.lua and add the following code to the scene:create
method.
function scene:create(event) --SNIP-- local invadersText = pulsatingText.new("INVADERZ",display.contentCenterX,display.contentCenterY-200,"Conquest", 20,group) invadersText:setColor( 1, 1, 1 ) invadersText:pulsate() end
We create an new TextField
instance with the word "INVADERZ", set its color, and call the pulsate
method. Notice how we passed the group
variable as a parameter to ensure the TextField
object gets added to this scene's view hierarchy.
I have included a font in the downloads named "Conquest" that has a futuristic look to it. Make sure you add it to your project folder if you want to use it. I downloaded the font from dafont.com, which is a great website for finding custom fonts. However, make sure you adhere to the license the font author has put in place.
To use the font, we also need to update the project's build.settings file. Take a look at the updated build.settings file.
settings = { orientation = { default ="portrait", supported = { "portrait" }, }, iphone = { plist= { UIAppFonts = { "Conquest.ttf" } }, }, }
If you test the project now, you should see the text was added to the scene and pulsates as expected.
4. Star Field Generator
To make the game a little more interesting, a moving star field is created in the background. To accomplish this, we do the same as we did with the PulsatingText
class and create a module. Create a file named starfieldgenerator.lua and add the following to it:
local starFieldGenerator= {} local starFieldGenerator_mt = {__index = starFieldGenerator} function starFieldGenerator.new(numberOfStars,theView,starSpeed) local starGroup = display.newGroup() local allStars ={} -- Table that holds all the stars for i=0, numberOfStars do local star = display.newCircle(math.random(display.contentWidth), math.random(display.contentHeight), math.random(2,8)) star:setFillColor(1 ,1,1) starGroup:insert(star) table.insert(allStars,star) end theView:insert(starGroup) local newStarFieldGenerator = { allStars = allStars, starSpeed = starSpeed } return setmetatable(newStarFieldGenerator,starFieldGenerator_mt) end function starFieldGenerator:enterFrame() self:moveStars() self:checkStarsOutOfBounds() end function starFieldGenerator:moveStars() for i=1, #self.allStars do self.allStars[i].y = self.allStars[i].y+self.starSpeed end end function starFieldGenerator:checkStarsOutOfBounds() for i=1, #self.allStars do if(self.allStars[i].y > display.contentHeight) then self.allStars[i].x = math.random(display.contentWidth) self.allStars[i].y = 0 end end end return starFieldGenerator
We first create the main table starFieldGenerator
and the metatable starFieldGenerator_mt
. In the new
method, we have a table allStars
that will be used to hold a reference to the stars that are created in the for loop. The number of iterations of the for loop is equal to numberOfStars
and we use the Display
object's newCircle
method to create a white circle.
We position the circle randomly within the game screen bounds and also give it a random size between 2 and 8. We insert each star into the allStars
table and place them into the view that was passed in as a parameter, which is the scene's view.
We set allStars
and starSpeed
as keys on the temporary table and then assign it as the metatable. We need access to the allStars
table and starSpeed
properties when we move the stars.
We'll use two methods to move the stars. The starFieldGenerator:moveStars
method does the moving of the stars while the starFieldGenerator:checkStarsOutOfBounds
method checks if the stars are out of the screen's bounds.
If the stars are out of the playing screen area, it generates a random x
position for that particular star and sets the y
position just above the top of the screen. By doing so, we are able to reuse the stars and it gives the illusion of a never-ending stream of stars.
We call these functions in the starFieldGenerator:enterFrame
method. By setting the enterFrame
method directly on this object, we can set this object as the context when we add the event listener.
Add the following code block to the scene:create
method in start.lua:
function scene:create(event) local group = self.view starGenerator = starFieldGenerator.new(200,group,5) startButton = display.newImage("new_game_btn.png",display.contentCenterX,display.contentCenterY+100) group:insert(startButton) end
Notice that we invoked the starGenerator.new
method when we are adding the startButton
. The order in which you add things to the scene does matter. If we were to add it after the start button, then some of the stars would have been on top of the button.
The order in which you add things to the scene is the order in which they will show up. There are a two methods of the Display
class, toFront
and toBack
, that can change this order.
If you test the game now, you should see the scene littered with random stars. They are not moving, however. We need to move them in the scene:show
method. Add the following to the scene:show
method of start.lua.
function scene:show(event) --SNIP-- if ( phase == "did" ) then startButton:addEventListener("tap",startGame) Runtime:addEventListener("enterFrame", starGenerator) end end
Here we add the enterFrame
event listener, which, if you recall, makes the stars move and checks if they are out of bounds.
Whenever you add an event listener, you should make sure you are also removing it at some point later in the program. The place to do that in this example is when the scene is removed. Add the following to the scene:hide
event.
unction scene:hide(event) local phase = event.phase if ( phase == "will" ) then startButton:removeEventListener("tap",startGame) Runtime:removeEventListener("enterFrame", starGenerator) end end
If you test the game now, you should see the stars moving and it will seem like an endless stream of stars. Once we add the player, it will also give the illusion of the player moving through space.
5. Game Level
When you press the startButton
button, you are taken to the game level scene, which is a blank screen at the moment. Let's fix that.
Step 1: Local Variables
Add the below code snippet to gamelevel.lua. You should make sure this code and all code from this point on is above where you are returning the scene
object. These are the local variables we need for the game level, most of which are self-explanatory.
local starFieldGenerator = require("starfieldgenerator") local pulsatingText = require("pulsatingtext") local physics = require("physics") local gameData = require( "gamedata" ) physics.start() local starGenerator -- an instance of the starFieldGenerator local player local playerHeight = 125 local playerWidth = 94 local invaderSize = 32 -- The width and height of the invader image local leftBounds = 30 -- the left margin local rightBounds = display.contentWidth - 30 - the right margin local invaderHalfWidth = 16 local invaders = {} -- Table that holds all the invaders local invaderSpeed = 5 local playerBullets = {} -- Table that holds the players Bullets local canFireBullet = true local invadersWhoCanFire = {} -- Table that holds the invaders that are able to fire bullets local invaderBullets = {} local numberOfLives = 3 local playerIsInvincible = false local rowOfInvadersWhoCanFire = 5 local invaderFireTimer -- timer used to fire invader bullets local gameIsOver = false; local drawDebugButtons = {} --Temporary buttons to move player in simulator local enableBulletFireTimer -- timer that enables player to fire
Step 2: Adding a Star Field
Like the previous scene, this scene also has a moving star field. Add the following to gamelevel.lua.
function scene:create(event) local group = self.view starGenerator = starFieldGenerator.new(200,group,5) end
We are adding a star field to the scene. As before, we need to make the stars move, which we do in the scene:show
method.
function scene:show(event) local phase = event.phase local previousScene = composer.getSceneName( "previous" ) composer.removeScene(previousScene) local group = self.view if ( phase == "did" ) then Runtime:addEventListener("enterFrame", starGenerator) end end
We are removing the previous scene and adding the enterFrame
event listener. As I mentioned earlier, whenever you add an event listener, you need to make sure you eventually remove it. We do this in the scene:hide
method.
function scene:hide(event) local phase = event.phase local group = self.view if ( phase == "will" ) then Runtime:removeEventListener("enterFrame", starGenerator) end end
Lastly, we should add the listeners for the create
, show
, and hide
methods. If you run the application now, you should have a moving star field.
scene:addEventListener( "create", scene ) scene:addEventListener( "show", scene ) scene:addEventListener( "hide", scene )
Step 3: Adding the Player
In this step, we'll add the player to the scene and get it moving. This game uses the accelerometer to move the player. We will also use an alternative way to move the player in the simulator by adding buttons to the scene. Add the following code snippet to gamelevel.lua.
function setupPlayer() local options = { width = playerWidth,height = playerHeight,numFrames = 2} local playerSheet = graphics.newImageSheet( "player.png", options ) local sequenceData = { { start=1, count=2, time=300, loopCount=0 } } player = display.newSprite( playerSheet, sequenceData ) player.name = "player" player.x=display.contentCenterX- playerWidth /2 player.y = display.contentHeight - playerHeight - 10 player:play() scene.view:insert(player) local physicsData = (require "shapedefs").physicsData(1.0) physics.addBody( player, physicsData:get("ship")) player.gravityScale = 0 end
The player is a SpriteObject
instance. By having the player be a sprite instead of a regular image, we can animate it. The player hastwo separate images, one with the thruster engaged and one with the thruster switched off.
By switching between the two images, we create the illusion of a never-ending thrust. We'll accomplish this with an image sheet, which is one large image composed of a number of smaller images. By cycling through the different images, you can create an animation.
The options table holds the width
, height
, and numFrames
of the individual images in the larger image. The numFrames
variable contains the value of the number of smaller images. The playerSheet
is an instance of the ImageSheet
object, which takes as parameters the image and the options table.
The sequenceData
variable is used by the SpriteObject
instance, the start
key is the image you wish to start the sequence or animation on while the count
key is how many total images there are in the animation. The time
key is how long it will take the animation to play and the loopCount
key is how many times you wish the animation to play or repeat. By setting loopCount
to 0
, it will repeat forever.
Lastly, you create the SpriteObject
instance by passing in the ImageSheet
instance and sequenceData
.
We give the player a name
key, which will be used to identify it later. We also set its x
and y
coordinates, invoke its play
method, and insert it into the scene's view.
We will be using Corona's built in physics engine, which uses the popular Box2d engine under the hood, to detect collisions between objects. The default collision detection uses a bounding box method of detecting collisions, which means it places a box around the object and uses that for collision detection. This works fairly well for rectangular objects, or circles by using a radius
property, but for oddly shaped objects it does not work out so well. Take a look at the below image to see what I mean.
You will notice that even though the laser is not touching the ship, it still registers as a collision. This is because it is colliding with the bounding box around the image.
To overcome this limitation, you can pass in a shape
parameter. The
shape parameter is a table of x
and y
coordinate pairs, with each pair
defining a vertex point for the shape. These shape parameter coordinates can be quite difficult to figure out by hand, depending on the complexity of the image. To overcome this, I use a program called PhysicsEditor.
The physicsData
variable is the file that was exported from PhysicsEditor. We call the addBody
method of the physics engine, passing in the player
and the physicsData
variable. The result is that the collision detection will use the actual bounds of the spaceship instead of using bounding box collision detection. The below image clarifies this.
You can see that even though the laser is within the bounding box, no collision is triggered. Only when it touches the object's edge will a collision be registered.
Lastly, we set gravityScale
to 0
on the player since we do not want it to be affected by gravity.
Now, invoke setupPlayer
in the scene:create
method.
function scene:create(event) local group = self.view starGenerator = starFieldGenerator.new(100,group,5) setupPlayer() end
If you run the game now, you should see the player added to the scene with its thruster engaged and activated.
Step 4: Moving the player
As mentioned earlier, we'll be moving the player using the accelerometer. Add the following code to gamelevel.lua.
local function onAccelerate(event) player.x = display.contentCenterX + (display.contentCenterX * (event.xGravity*2)) end system.setAccelerometerInterval( 60 ) Runtime:addEventListener ("accelerometer", onAccelerate)
The onAccelerate
function will be called each time the accelerometer interval is fired. It is set to fire 60 times per second. It's important to know that the accelerometer can be a big drain on the device's battery. In other words, if you are not using it for an extended period of time, it would be wise to remove the event listener from it.
If you test on a device, you should be able to move the player by tilting the device. This doesn't work when testing in the simulator however. To remedy this, we'll create a few temporary buttons.
Step 5: Debug Buttons
Add the following code to draw the debug buttons to the screen.
function drawDebugButtons() local function movePlayer(event) if(event.target.name == "left") then player.x = player.x - 5 elseif(event.target.name == "right") then player.x = player.x + 5 end end local left = display.newRect(60,700,50,50) left.name = "left" scene.view:insert(left) local right = display.newRect(display.contentWidth-60,700,50,50) right.name = "right" scene.view:insert(right) left:addEventListener("tap", movePlayer) right:addEventListener("tap", movePlayer) end
This code uses the Display
's newRect
method to draw two rectangles to the screen. We then add a tap even listener to them that calls the local movePlayer
function.
6. Firing Bullets
Step 1: Adding and Moving Bullets
When the user taps the screen, the player's ship will fire a bullet. We will be limiting how often the user can fire a bullet by using a simple timer. Take a look at the implementation of the firePlayerBullet
function.
function firePlayerBullet() if(canFireBullet == true)then local tempBullet = display.newImage("laser.png", player.x, player.y - playerHeight/ 2) tempBullet.name = "playerBullet" scene.view:insert(tempBullet) physics.addBody(tempBullet, "dynamic" ) tempBullet.gravityScale = 0 tempBullet.isBullet = true tempBullet.isSensor = true tempBullet:setLinearVelocity( 0,-400) table.insert(playerBullets,tempBullet) local laserSound = audio.loadSound( "laser.mp3" ) local laserChannel = audio.play( laserSound ) audio.dispose(laserChannel) canFireBullet = false else return end local function enableBulletFire() canFireBullet = true end timer.performWithDelay(750,enableBulletFire,1) end
We first check if the user is able to fire a bullet. We then create a bullet and give it a name
property so we can identify it later. We add it as a physics body and give it the type dynamic since it will be moving with a certain velocity.
We set the gravityScale
to 0
, because we do not want it to be affected by gravity, set the isBullet
property to true
, and set it to be sensor for collision detection. Lastly, we call setLinearVelocity
to get the bullet moving on vertically. You can find out more about these properties in the documentation for Physics Bodies.
We load and play a sound, and then immediately release the memory associated with that sound. It's important to release the memory from sound objects when they are no longer in use. We set canFireBullet
to false
and start a timer that sets it back to true
after a short time.
We now need to add the tap
listener to the Runtime
. This is different from adding a tap listener to an individual object. No matter where you tap on the screen, the Runtime
listener is fired. This is because the Runtime
is the global object for listeners.
function scene:show(event) --SNIP-- if ( phase == "did" ) then Runtime:addEventListener("enterFrame", starGenerator) Runtime:addEventListener("tap", firePlayerBullet) end end
We also need to make sure that we remove this event listener when we no longer need it.
function scene:hide(event) if ( phase == "will" ) then Runtime:removeEventListener("enterFrame", starGenerator) Runtime:removeEventListener("tap", firePlayerBullet) end end
If you test the game and tap the screen, a bullet should be added to the screen and move to the top of the device. There is a problem though. Once the bullet moves off-screen, they keep moving forever. This is not very useful for the game's memory. Imagine having hundreds of bullets off-screen, moving into infinity. It would take up unnecessary resources. We'll fix this issue in the next step.
Step 2: Removing Bullets
Whenever a bullet is created, it is stored in the playerBullets
table. This makes it easy to reference each bullet and check its properties. What we will do is loop through the playerBullets
table, check its y
property, and, if it is off-screen, remove it from the Display
and from the playerBullet
table.
function checkPlayerBulletsOutOfBounds() if(#playerBullets > 0)then for i=#playerBullets,1,-1 do if(playerBullets[i].y < 0) then playerBullets[i]:removeSelf() playerBullets[i] = nil table.remove(playerBullets,i) end end end end
An important point to note is that we are looping through the playersBullet
table in reverse order. If we were to loop through the table in normal fashion, when we remove an object it would throw the index off and cause a processing error. By looping through the table in reverse order, the object has already been processed. Also important to note is that when you remove an object from the Display
, it should be set to nil
.
Now we need a place to call this function. The most common way to do this is to create a game loop. If you are unfamiliar with the concept of the game loop, you should read this short article by Michael James Williams. We'll implement the game loop in the next step.
Step 3: Create the Game Loop
Add the following code to gamelevel.lua to get started.
function gameLoop() checkPlayerBulletsOutOfBounds() end
We need to call this function repeatedly for as long as the game is running. We will do this by using the Runtime
's enterFrame
event. Add the following in the scene:show
function.
function scene:show(event) --SNIP-- if ( phase == "did" ) then Runtime:addEventListener("enterFrame", gameLoop) Runtime:addEventListener("enterFrame", starGenerator) Runtime:addEventListener("tap", firePlayerBullet) end end
We need to make sure we remove this event listener, when we leave this scene. We do this in the scene:hide
function.
function scene:hide(event) if ( phase == "will" ) then Runtime:removeEventListener("enterFrame", gameLoop) Runtime:removeEventListener("enterFrame", starGenerator) Runtime:removeEventListener("tap", firePlayerBullet) end end
7. Invaders
Step 1: Adding Invaders
In this step, we will add the invaders. Start by adding the following code block.
function setupInvaders() local xPositionStart =display.contentCenterX - invaderHalfWidth - (gameData.invaderNum *(invaderSize + 10)) local numberOfInvaders = gameData.invaderNum *2+1 for i = 1, gameData.rowsOfInvaders do for j = 1, numberOfInvaders do local tempInvader = display.newImage("invader1.png",xPositionStart + ((invaderSize+10)*(j-1)), i * 46 ) tempInvader.name = "invader" if(i== gameData.rowsOfInvaders)then table.insert(invadersWhoCanFire,tempInvader) end physics.addBody(tempInvader, "dynamic" ) tempInvader.gravityScale = 0 tempInvader.isSensor = true scene.view:insert(tempInvader) table.insert(invaders,tempInvader) end end end
Depending on which level the player is on, the rows will contain a different number of invaders. We set how many rows to create when we add the rowsOfInvaders
key to the gameData
table (3
). The invaderNum
is used to keep track of which level we're on, but it is also used in some calculations.
To get the starting x
position for the invader, we subtract half the invader's width from the screen's center. We then subtract whatever (invaderNum * invaderSize + 10)
is equal to. There is an offset of ten pixels between each invader, which is why we are adding to the invaderSize
. That might all seem a little confusing so take your time to understand it.
We determine how many invaders there are per row by taking invaderNum * 2
and adding 1
to it. For example, on the first level invaderNum
is 1
so we will have three invaders per row (1 * 2 + 1
). On the second level, there will be five invaders per row, (2 * 2 + 1
), etc.
We use nested for loops to set up the rows and columns respectively. In the second for loop we create the invader. We give it a name
property so we can reference it later. If i
is equal to the gameData.rowsOfInvaders
, then we add the invader to the invadersWhoCanFire
table. This ensures all invaders on the bottom row start out as being able to fire bullets. We set the physics up in the same way as the we did with the player
earlier, and insert the invader
into the scene and into the invaders
table so we can reference it later.
Step 2: Moving Invaders
In this step, we will move the invaders. We will use the gameLoop
to check the invaders's position and reverse their direction if necessary. Add the following code block to get started.
function moveInvaders() local changeDirection = false for i=1, #invaders do invaders[i].x = invaders[i].x + invaderSpeed if(invaders[i].x > rightBounds - invaderHalfWidth or invaders[i].x < leftBounds + invaderHalfWidth) then changeDirection = true; end end if(changeDirection == true)then invaderSpeed = invaderSpeed*-1 for j = 1, #invaders do invaders[j].y = invaders[j].y+ 46 end changeDirection = false; end end
We loop through the invaders and change their x
position by the value stored in the invaderSpeed
variable. We see if the invader is out of bounds by checking leftBounds
and rightBounds
, which we set up earlier.
If an invader is out of bounds, we set changeDirection
to true
. If changeDirection
is set to true
, we negate the invaderSpeed
variable, move the invaders down on the y
axis by 16 pixels, and reset the changeDirection
variable to false
.
We invoke the moveInvaders
function in the gameLoop
function.
function gameLoop() checkPlayerBulletsOutOfBounds() moveInvaders() end
8. Detecting Collisions
Now that we have some invaders on screen and moving, we can check for collisions between any of the player's bullets and the invaders. We perform this check in the onCollision
function.
function onCollision(event) local function removeInvaderAndPlayerBullet(event) local params = event.source.params local invaderIndex = table.indexOf(invaders,params.theInvader) local invadersPerRow = gameData.invaderNum *2+1 if(invaderIndex > invadersPerRow) then table.insert(invadersWhoCanFire, invaders[invaderIndex - invadersPerRow]) end params.theInvader.isVisible = false physics.removeBody( params.theInvader ) table.remove(invadersWhoCanFire,table.indexOf(invadersWhoCanFire,params.theInvader)) if(table.indexOf(playerBullets,params.thePlayerBullet)~=nil)then physics.removeBody(params.thePlayerBullet) table.remove(playerBullets,table.indexOf(playerBullets,params.thePlayerBullet)) display.remove(params.thePlayerBullet) params.thePlayerBullet = nil end end if ( event.phase == "began" ) then if(event.object1.name == "invader" and event.object2.name == "playerBullet")then local tm = timer.performWithDelay(10, removeInvaderAndPlayerBullet,1) tm.params = {theInvader = event.object1 , thePlayerBullet = event.object2} end if(event.object1.name == "playerBullet" and event.object2.name == "invader") then local tm = timer.performWithDelay(10, removeInvaderAndPlayerBullet,1) tm.params = {theInvader = event.object2 , thePlayerBullet = event.object1} end end end
There are two ways to do collision detection using Corona's built-in physics engine. One way is to register for the collision on the objects themselves. The other way is to listen globally. We use the global approach in this tutorial.
In the onCollision
method, we check the name
properties of the objects, set a small delay, and invoke the removeInvaderAndPlayerBullet
function. Because we do not know what event.object1
and event.object2
will point to, we have to check both situations hence the two opposite if statements.
We send along some parameters with the timer so we can identify the playerBullet
and the invader
within the removePlayerAndBullet
function. Whenever you are modifying an object's properties in a collision check, you should apply a small delay before doing so. This is the reason for the short timer.
Inside the removeInvaderAndPlayerBullet
function, we get a reference to the params
key. We then get the index of the invader
within the invaders
table. Next, we determine how many invaders there are per row. If this number it is greater than invadersPerRow
, we determine which invader to add to the invadersWhoCanFire
table. The idea is that whichever invader was hit, the invader in the same column one row up can now fire.
We then set the invader
to not be visible, remove its body from the physics engine, and remove it from theinvadersWhoCanFire
table.
We remove the bullet from the physics engine, remove it from the playerBullets
table, remove it from display, and set it to nil
to be certain it is marked for garbage collection.
To make all this work, we need to listen for collision events. Add the following code to the scene:show
method.
function scene:show(event) local phase = event.phase local previousScene = composer.getSceneName( "previous" ) composer.removeScene(previousScene) local group = self.view if ( phase == "did" ) then Runtime:addEventListener("enterFrame", gameLoop) Runtime:addEventListener("enterFrame", starGenerator) Runtime:addEventListener("tap", firePlayerBullet) Runtime:addEventListener( "collision", onCollision ) end end
We need to make sure we remove this event listener when we leave the scene. We do this in the scene:hide
method.
function scene:hide(event) if ( phase == "will" ) then Runtime:removeEventListener("enterFrame", starGenerator) Runtime:removeEventListener("tap", firePlayerBullet) Runtime:removeEventListener("enterFrame", gameLoop) Runtime:removeEventListener( "collision", onCollision ) end end
If you test the game now, you should be able to fire a bullet, hit an invader, and have both the bullet and the invader removed from the scene.
Conclusion
This brings this part of the series to a close. In the next and final part of this series, we will make the invaders fire bullets, make sure the player can die, and handle game over as well as new levels. I hope to see you there.