Quantcast
Channel: Envato Tuts+ Code - Mobile Development
Viewing all articles
Browse latest Browse all 1836

Create a Space Invaders Game in Corona: Implementing Gameplay

$
0
0
Final product image
What You'll Be Creating

In the first part of this series, we se tup 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)
		theView:insert(starGroup)
		table.insert(allStars,star)
    end
	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 enterFramemethod 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))
		physics.removeBody(params.thePlayerBullet)
	  	table.remove(playerBullets,table.indexOf(playerBullets,params.thePlayerBullet))
	  	display.remove(params.thePlayerBullet)
	  	params.thePlayerBullet = nil
	  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.

2015-01-07T17:15:29.000Z2015-01-07T17:15:29.000ZJames Tyner

Viewing all articles
Browse latest Browse all 1836

Trending Articles