Introduction
In the previous tutorial of this series, we started implementing the game's gameplay and already managed to get the plane moving around on the screen. In this tutorial, we'll continue implementing the gameplay. Let's dive right in with the startTimers
function.
1. startTimers
As its name indicates, the startTimers
function starts the timers. Add the following code to gamelevel.lua.
function startTimers() end
Invoke this function in the enterScene
method as shown below.
function scene:enterScene( event ) local planeSound = audio.loadStream("planesound.mp3") planeSoundChannel = audio.play( planeSound, {loops=-1} ) Runtime:addEventListener("enterFrame", gameLoop) startTimers() end
2. firePlayerBullet
The firePlayerBullet
function creates a bullet for the player.
function firePlayerBullet() local tempBullet = display.newImage("bullet.png",(player.x+playerWidth/2) - bulletWidth,player.y-bulletHeight) table.insert(playerBullets,tempBullet); planeGroup:insert(tempBullet) end
Here we use the Display object's newImage
method to create the bullet. We position it in such a way that it's in the center of the plane on the x axis and at the very top of the plane on the y axis. The bullet is then inserted into the playerBullets
table for later reference and also into the planeGroup
.
3. Calling firePlayerBullet
We need to call the firePlayerBullet
function periodically to make sure the player's plane is automatically firing bullets. Add the following code snippet in the startTimers
function.
function startTimers() firePlayerBulletTimer = timer.performWithDelay(2000, firePlayerBullet ,-1) end
As its name indicates, the Timer's performWithDelay
method calls a specified function after a period of time has passed. The time is in milliseconds, so here we are calling the firePlayerBullet
function every two seconds. By passing -1
as the third argument, the timer will repeat forever.
If you test the game now, you should see that every two seconds a bullet appears. However, they aren't moving yet. We will take care of that in the next few steps.
4.movePlayerBullets
In movePlayerBullets
, we loop through the playerBullets
table and change the y
coordinate of every bullet. We first check to make sure the playerBullets
table has bullets in it. The #
before playerBullets
is called the length operator and it returns the length of the object it is called upon. It's useful to know that the #
operator also works on strings.
function movePlayerBullets() if(#playerBullets > 0) then for i=1,#playerBullets do playerBullets[i]. y = playerBullets[i].y - 7 end end end
We need to invoke movePlayerBullets
in the gameLoop
function as shown below.
function gameLoop() --SNIP-- numberOfTicks = numberOfTicks + 1 movePlayer() movePlayerBullets() end
5. checkPlayerBulletsOutOfBounds
When a bullet goes off-screen, it is no longer relevant to the game. However, they're still part of the playerBullets
table and continue to move like any other bullet in the table. This is a waste of resources and, if the game were to go on for a very long time, it would result in hundreds or thousands of unused objects.
To overcome this, we monitor the bullets and, once they move off-screen, we remove them from the playerBullets
table as well as from from the display. Take a look at the implementation of checkPlayerBulletsOutOfBounds
.
function checkPlayerBulletsOutOfBounds() if(#playerBullets > 0) then for i=#playerBullets,1,-1 do if(playerBullets[i].y < -18) then playerBullets[i]:removeSelf() playerBullets[i] = nil table.remove(playerBullets,i) end end end end
It's important to note that we are looping through the playerBullets
table in backwards. If we loop through the table forwards, then, when we remove one of the bullets, it would throw the looping index off and causing an error. By looping over the table in reverse order, the last bullet is already processed. The removeSelf
method removes the display object and frees its memory. As a best practice, you should set any objects to nil
after calling removeSelf
.
We invoke this function in the gameLoop
function.
function gameLoop() --SNIP-- movePlayer() movePlayerBullets() checkPlayerBulletsOutOfBounds() end
If you want to see if this function is working properly, you can temporarily insert a print("Removing Bullet")
statement immediately after setting the display object to nil
.
6. generateIsland
To make the game more interesting, we generate an island every so often, and move it down the screen to give the appearance of the plane flying over the islands. Add the following code snippet for the generateIsland
function.
function generateIsland() local tempIsland = display.newImage("island1.png", math.random(0,display.contentWidth - islandWidth),-islandHeight) table.insert(islands,tempIsland) islandGroup:insert( tempIsland ) end
We make use of the newImage
method once again and position the island by setting a negative value for the islandHeight
. For the x
position, we use the math.random
method to generate a number between 0
and the display
's contentWidth
minus the islandWidth
. The reason we subtract the width of the island is to make sure the island is completely on the screen. If we wouldn't subtract the island's width, there would be a chance that part of the island wouldn't be on the screen.
We need to start a timer to generate an island every so often. Add the following snippet to the startTimers
function we created earlier. As you can see, we are generating an island every five seconds. In the next step, we'll make the islands move.
function startTimers() firePlayerBulletTimer = timer.performWithDelay(2000, firePlayerBullet ,-1) generateIslandTimer = timer.performWithDelay( 5000, generateIsland ,-1) end
7.moveIslands
The implementation of moveIslands
is nearly identical to the movePlayerBullets
function. We check if the islands
table contains any islands and, if it does, we loop through it and move each island a little bit.
function moveIslands() if(#islands > 0) then for i=1, #islands do islands[i].y = islands[i].y + 3 end end end
8.checkIslandsOutOfBounds
Just like we check if the player's bullets have moved off-screen, we check if any of the islands had moved off-screen. The implementation of checkIslandsOutOfBounds
should therefore look familiar to you. We check if the islands y
position is greater than display.contentHeight
and if it is, we know the island has moved off-screen and can therefore be removed.
function checkIslandsOutOfBounds() if(#islands > 0) then for i=#islands,1,-1 do if(islands[i].y > display.contentHeight) then islands[i]:removeSelf() islands[i] = nil table.remove(islands,i) end end end end
9. generateFreeLife
Every so often, the player has a chance to get a free life. We first generate a free life image and if the player collides with the image they get an extra life. The player can have a maximum of six lives.
function generateFreeLife () if(numberOfLives >= 6) then return end local freeLife = display.newImage("newlife.png", math.random(0,display.contentWidth - 40), 0); table.insert(freeLifes,freeLife) planeGroup:insert(freeLife) end
If the player already has six lives, we do nothing by returning early from the function. If not, we create a new life image and add it to the screen. Similar to how we positioned the islands earlier, we set the image at a negative y
position and generate a random value for the image's x
position. We then insert it into the freeLifes
table to be able to reference it later.
We need to call this function every so often. Add the following snippet to the startTimers
function.
function startTimers() firePlayerBulletTimer = timer.performWithDelay(2000, firePlayerBullet ,-1) generateIslandTimer = timer.performWithDelay( 5000, generateIsland ,-1) generateFreeLifeTimer = timer.performWithDelay(7000,generateFreeLife, - 1) end
10.moveFreeLives
The implementation of moveFreeLifes
should look familiar. We are looping through the freeLifes
table and move every image in it.
function moveFreeLifes() if(#freeLifes > 0) then for i=1,#freeLifes do freeLifes[i].y = freeLifes[i].y +5 end end end
All we need to do is call moveFreeLifes
in the gameLoop
function.
function gameLoop() --SNIP-- checkIslandsOutOfBounds() moveFreeLifes() end
11. checkFreeLifesOutOfBounds
The following code snippet should also look familiar to you by now. We check if any of the images in the freeLifes
table have moved off-screen and remove the ones that have.
function checkFreeLifesOutOfBounds() if(#freeLifes > 0) then for i=#freeLifes,1,-1 do if(freeLifes[i].y > display.contentHeight) then freeLifes[i]:removeSelf() freeLifes[i] = nil table.remove(freeLifes,i) end end end end
We call this function in the gameLoop
function.
function gameLoop() --SNIP-- checkIslandsOutOfBounds() moveFreeLifes() checkFreeLifesOutOfBounds() end
12. hasCollided
We need to be able to tell when game objects collide with each other, such as the player's plane and the free life images, the bullets and the planes, etc. While Corona offers a very robust physics engine that can easily handle collisions between display objects for us, doing so adds a bit of overhead with the calculations the engine has to do every frame.
For the purposes of this game, we will be using a simple bounding box collision detection system. What this function does, is make sure the rectangles or bounding boxes around two objects don't overlap. If they do, the objects are colliding. This logic is implemented in the hasCollided
function.
function hasCollided( obj1, obj2 ) if ( obj1 == nil ) then return false end if ( obj2 == nil ) then return false end local left = obj1.contentBounds.xMin <= obj2.contentBounds.xMin and obj1.contentBounds.xMax >= obj2.contentBounds.xMin local right = obj1.contentBounds.xMin >= obj2.contentBounds.xMin and obj1.contentBounds.xMin <= obj2.contentBounds.xMax local up = obj1.contentBounds.yMin <= obj2.contentBounds.yMin and obj1.contentBounds.yMax >= obj2.contentBounds.yMin local down = obj1.contentBounds.yMin >= obj2.contentBounds.yMin and obj1.contentBounds.yMin <= obj2.contentBounds.yMax return (left or right) and (up or down) end
I found this code snippet on the CoronaLabs website. It works really well, because the game objects in our game are rectangular. If you're working with object that aren't rectangular, then you better take advantage of Corona's physics engine as its collision detection is very well optimized for this.
13. checkPlayerCollidesWithFreeLife
We want to check if the player's plane has collided with a free life object. If it has, then we award the player a free life.
function checkPlayerCollidesWithFreeLife() if(#freeLifes > 0) then for i=#freeLifes,1,-1 do if(hasCollided(freeLifes[i], player)) then freeLifes[i]:removeSelf() freeLifes[i] = nil table.remove(freeLifes, i) numberOfLives = numberOfLives + 1 hideLives() showLives() end end end end
In the checkPlayerCollidesWithFreeLife
function, we loop through the freeLives
table backwards for the same reason I described earlier. We call the hasCollided
function and pass in the current image and the player's plane. If the two object collide, we remove the free life image, increment the numberOfLives
variable, and call the hideLives
and showLives
function.
We invoke this function in the gameLoop
function.
function gameLoop() --SNIP-- moveFreeLifes() checkFreeLifesOutOfBounds() checkPlayerCollidesWithFreeLife() end
14. hideLives
The hideLives
function loops through the livesImages
table and sets the isVisible
property of each life image to false
.
function hideLives() for i=1, 6 do livesImages[i].isVisible = false end end
15. showLives
The showLives
function loops through the livesImages
table and sets each image's isVisible
property to true
.
function showLives() for i=1, numberOfLives do livesImages[i].isVisible = true; end end
Conclusion
This brings the third part of this series to a close. In the next and final installment of this series, we will create enemy planes and finalized the game's gameplay. Thanks for reading and see you there.