Advertisement

Create a Plane Fighting Game in Corona: More Gameplay

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

This post is part of a series called Create a Plane Fighting Game in Corona.
Create a Plane Fighting Game in Corona: Gameplay
Create a Plane Fighting Game in Corona: Finishing Gameplay
Final product image
What You'll Be Creating

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.

Advertisement