Advertisement

Create a Space Defender Game – Game Logic

by

In this tutorial series, we will be learning how to create a space shooter game just like the classic game Space Defender. Read on!


Series Overview

In this version of Space Defender, the player will have to defend his space by shooting enemies. Every time the player successfully destroys an enemy, they will earn points and when the player has reached 20 or 40 points, their gun will receive an upgrade. To mix things up, this game will send out bonus packages that are worth 5 points. To see the game in action, watch the short video above.


Where We Left Off…

In part 1 of this series, we learned how to set up our app, how to use custom fonts, how to use a storyboard, and how to set up our main menu. In Part 2 of this series, we will learn how to create the gameplay of our app. So, let’s get started!

Our first step is to create a new file called game.lua. Once it’s created, open the file in your favorite editor.


1. Adding Libraries

Since we are starting a new scene, we have to require some libraries. We will be using the physics engine built into Corona SDK for collision detection.

local storyboard = require( "storyboard" )

local scene = storyboard.newScene()

local physics = require("physics")

2. Configuring Physics

After we have our libraries setup, we will configure the physics engine. The settings below will set the gravity to 0 (just like in space) and set the iterations to 16. The setPositionIterations means the engine will go through 16 positions per frame. Anything higher than 16 can adversely affect game performance.

physics.start()

physics.setGravity(0, 0)

physics.setPositionIterations( 16 )

3. Using A Random Generator

Although this step is not necessary for this tutorial, it's a good practice to "seed" the random number generator. I like to use the current time to seed the generator.

math.randomseed( os.time() )

4. Setting Up Game Variables

Now we will define some variables for our game. Each variable has a comment next to it explaining the purpose of the variable.

local screenW, screenH, halfW, halfY = display.contentWidth, display.contentHeight, display.contentWidth*0.5, display.contentHeight*0.5

local gameover_returntomenu -- forward declard our game over button

-- Set up game settings

motionx = 0; -- Variable used to move character along y axis

speed = 10; -- Controls the ship speed

playerScore = 0; -- Sets the player score

playerLives = 20; -- Sets the number of lives for the player

slowEnemySpeed = 2375; -- Sets how fast the white ships move across screen

slowEnemySpawn = 2400; -- Sets white ship spawn rate

fastEnemySpeed = 1875; -- Sets how fast the green ships move across screen

fastEnemySpawn = 1800; -- Sets green ship spawn rate

bulletSpeed = 325; -- Sets how fast the bullet travels across screen

bulletSpawn = 250; -- Sets bullet spawn rate

5. Create the Main Game Scene

After creating the variables, we are going setup the scene inside the function scene:createScene. If you remember from Part 1, this function is used to create visual elements and game logic. In a later function, we will call upon these functions to run the game.

In the following code, we are creating the scene:createScene function and adding the background and top/bottom walls. Both walls are set up as static physics objects to prevent the player from going off screen.

function scene:createScene( event )

local group = self.view

-- Set up visual elements and walls

local bg = display.newImageRect("images/BKG.png", 480, 320)

  bg.x = halfW

  bg.y = halfY

  group:insert(bg)

local topwall = display.newRect(0,0,screenW,20)

  topwall.y = -5

  topwall:setFillColor(0,0,0)

  topwall.alpha = 0.01

  physics.addBody( topwall, "static" )

  group:insert(topwall)

local bottomwall = display.newRect(0,0,screenW,20)

  bottomwall.y = 325

  bottomwall:setFillColor(0,0,0)

  bottomwall.alpha = 0.01

  physics.addBody( bottomwall, "static" )

  group:insert(bottomwall)

end

6. Adding HUD Elements

Inside of the same scene:createScene function, but after the bottomwall display object, we are going to add four more display objects. Here’s an explanation of the purpose of each object.

  • btn_up, btn_down: These display objects will act as buttons on the left hand side of the screen and each object will move the ship up or down respectively. However, they are not operable until we set up the move function.
  • enemyHitBar: This display object is set up as a sensor and will only react to physic collisions. When it does react to collisions, it will remove the enemy object and subtract one from player lives.
local btn_up = display.newRect(0,0,75,160)

  btn_up:setReferencePoint(display.TopLeftReferencePoint)

  btn_up.x, btn_up.y = 0,0;

  btn_up.alpha = 0.01

  group:insert(btn_up)

local btn_down = display.newRect(0,0,75,160)

  btn_down:setReferencePoint(display.BottomLeftReferencePoint)

  btn_down.x, btn_down.y = 0, screenH;

  btn_down.alpha = 0.01

  group:insert(btn_down)

local enemyHitBar = display.newRect(-20,0,20,320)

  enemyHitBar:setFillColor(0,0,0)

  enemyHitBar.name = "enemyHitBar"

  physics.addBody( enemyHitBar, { isSensor = true } )

  group:insert(enemyHitBar)

Right after the enemyHitBar display object, we are going to add some GUI elements to display the player score and player lives. We will also show text on the screen that says "Move Up" and "Move Down" to notify the player where they need to touch to move the ship up or down.

local gui_score = display.newText("Score: "..playerScore,0,0,"Kemco Pixel",16)

  gui_score:setReferencePoint(display.TopRightReferencePoint)

  gui_score.x = screenW

  group:insert(gui_score)

local gui_lives = display.newText("Lives: "..playerLives,0,0,"Kemco Pixel",16)

  gui_lives:setReferencePoint(display.BottomRightReferencePoint)

  gui_lives.x = screenW

  gui_lives.y = screenH

  group:insert(gui_lives)

local gui_moveup = display.newText("Move Up",0,0,50,100,"Kemco Pixel",16)

  group:insert(gui_moveup)

local gui_movedown = display.newText("Move Down",0,0,50,23,"Kemco Pixel",16)

  gui_movedown:setReferencePoint(display.BottomLeftReferencePoint)

  gui_movedown.y = screenH

  group:insert(gui_movedown)

7. Adding the Player Ship

Next, we will be adding the player’s ship to the screen. The ship will be added as a dynamic physics object so it can react to collisions with other physics objects. We will go into further depth on collisions later in this tutorial.

local ship = display.newImageRect("images/spaceShip.png", 29, 19)

  ship.x, ship.y = 75, 35

  ship.name = "ship"

  physics.addBody( ship, "dynamic", { friction=0.5, bounce=0 } )

  group:insert(ship)

8. Moving the Player Ship

Do you remember the btn_up and btn_down display objects we added? We are now going to add event listeners to these objects to help make the player ship move. When btn_up is touched, we will make our speed variable negative and when btn_down is touched we will make our speed positive. By making this variable positive and negative, we are telling our next function to move the ship up or down.

-- When the up button is touched, set our motion to move the ship up

function btn_up:touch()

  motionx = -speed;

end

btn_up:addEventListener("touch",btn_up)

-- When the down button is touched, set our motion to move the ship down

function btn_down:touch()

  motionx = speed;

end

btn_down:addEventListener("touch",btn_down)

9. Enabling Movement

After we've added event listeners to our btn_up and btn_down display objects, we are going to create two runtime event listeners with their respective functions. These functions will run every frame and the one catch with runtime functions is that you must specify when to stop them. We’ll cover that later. For now, the stop function will set the variable motionx to 0 (because neither button is touched) and the moveguy function will add the variable motionx to our ship’s y position.

local function stop (event)

  if event.phase =="ended" then

    motionx = 0;

  end

end

Runtime:addEventListener("touch", stop )

-- This function will actually move the ship based on the motion

local function moveguy (event)

  ship.y = ship.y + motionx;

end

Runtime:addEventListener("enterFrame", moveguy)

10. Firing Bullets

By now, we have our ship moving, but it’s not firing! To get the ship ready to fire bullets, we have to create the fireShip() function. This function will create new display objects that react to physics collisions and this function will also move the object across the screen from left to right.

To make the game more interesting, we will allow the player to shoot more bullets when they reach a certain score. When the player reaches 20, the ship will shoot two bullets and when the player reaches 40, the ship will fire a third bullet that shoots downward diagonally.

function fireShip()

  bullet = display.newImageRect("images/bullet.png", 13, 8)

  bullet.x = ship.x + 9

  bullet.y = ship.y + 6

  bullet:toFront()

  bullet.name = "bullet"

  physics.addBody( bullet, { isSensor = true } )

  transition.to(bullet, {time = bulletSpeed, x = 500,

    onComplete = function (self)

    self.parent:remove(self); self = nil;

  end; })

  if(playerScore >= 20) then

    secondBullet = display.newImageRect("images/bullet.png", 13, 8)

    secondBullet.x = ship.x + 9

    secondBullet.y = ship.y + 12

    secondBullet:toFront()

    secondBullet.name = "bullet"

    physics.addBody( secondBullet, { isSensor = true } )

    transition.to(secondBullet, {time = bulletSpeed, x = 500,

    onComplete = function (self)

      self.parent:remove(self); self = nil;

    end; })

  end

  if(playerScore >= 40) then

    thirdBullet = display.newImageRect("images/bullet.png", 13, 8)

    thirdBullet.x = ship.x + 9

    thirdBullet.y = ship.y + 12

    thirdBullet:toFront()

    thirdBullet.name = "bullet"

    physics.addBody( thirdBullet, { isSensor = true } )

    transition.to(thirdBullet, {time = bulletSpeed, x = 500, y = ship.y + 100,

    onComplete = function (self)

      self.parent:remove(self); self = nil;

    end; })

11. Creating Enemies

After we’ve set up our ship to fire, we need to give the player some enemies to shoot at! We’ll create two different functions – createSlowEnemy() and createFastEnemy(). Both functions will create a physics display object that moves from right to left with the speed of the enemy and the image being the only difference.

function createSlowEnemy()

  enemy = display.newImageRect("images/enemy.png", 32, 26)

  enemy.rotation = 180

  enemy.x = 500

  enemy.y = math.random(10,screenH-10)

  enemy.name = "enemy"

  physics.addBody( enemy, {isSensor = true } )

  transition.to(enemy, {time = slowEnemySpeed, x = -20 })

end

function createFastEnemy()

  enemy = display.newImageRect("images/fastEnemy.png", 32, 26)

  enemy.rotation = 180

  enemy.x = 500

  enemy.y = math.random(10,screenH-10)

  enemy.name = "enemy"

  physics.addBody( enemy, {isSensor = true } )

  transition.to(enemy, {time = fastEnemySpeed, x = -20 })

end

12. Create Bonus Packages

Next, we’ll create bonus packages for our player to grab inside the function createBonus(). The createBonus() function will create a physics display objects that moves right to left and each bonus package the player grabs, they will earn 5 points.

function createBonus()

  bonus = display.newImageRect("images/bonus.png", 18, 18)

  bonus.rotation = 180

  bonus.x = 500

  bonus.y = math.random(10,screenH-10)

  bonus.name = "bonus"

  physics.addBody( bonus, {isSensor = true } )

  transition.to(bonus, {time = 1475, x = -20,

  onComplete = function ()

    display.remove(bonus)

    bonus = nil

  end; })

end

13. Updating Player Lives

Our next to last function is the updateLives() function. This function will be called every time an enemy gets past the player to give the player the goal of defending his side of space. If the number of lives is above 0, then this function will subtract one life and update the on screen text. Otherwise, it will result in a game over scene.

In the game over scene, we are canceling all of our timers and remove all of our event listeners. With the Corona SDK, it’s very important to remember that you have to explicitly tell your app when to remove runtime listeners and timers (only when the timer is running). After these have been removed, we will display a game over message and allow the player to return to the menu.

function updateLives()

  if(playerLives >= 0) then

    playerLives = playerLives - 1

    gui_lives.text = "Lives: "..playerLives

    gui_lives.x = screenW

  else

    timer.cancel(tmr_fireShip)

    timer.cancel(tmr_sendSlowEnemies)

    timer.cancel(tmr_sendSlowEnemies2)

    timer.cancel(tmr_sendFastEnemies)

    timer.cancel(tmr_sendBonus)

    Runtime:removeEventListener( "collision", onCollision )

    Runtime:removeEventListener("enterFrame", moveguy)

    Runtime:removeEventListener("touch", stop )

    -- Display game over screen

    local gameover_message = display.newText("Game Over!",0,0,"Kemco Pixel",32)

    gameover_message.x = halfW

    gameover_message.y = halfY - 15

    group:insert(gameover_message)

    function returnToMenuTouch(event)

    if(event.phase == "began") then

      storyboard.gotoScene("menu", "slideRight", "1000")

    end

  end

  gameover_returntomenu = display.newText("Return To Menu",0,0,"Kemco Pixel", 28)

  gameover_returntomenu.x = halfW

  gameover_returntomenu.y = halfY + 35

  gameover_returntomenu:addEventListener("touch", returnToMenuTouch)

  group:insert(gameover_returntomenu)

  end

end

14. Collision Detection

We are ready for our final function inside of our scene:createScene() function! This function will handle all of our collision detection by comparing the property myName of object1 to that held by object 2. Each object is passed as a parameter to this function under the variable name event.

To make it easier for you, I’ve broken down the five collision cases.

  • Case 1 – Object 1 is a bullet and Object 2 is an enemy
    What’s happening: Remove enemy and update score
  • Case 2 – Object 1 is an enemy and Object 2 is an bullet
    What’s happening: Remove enemy and update score
  • Case 3 – Object 1 is a ship and Object 2 is an bonus
    What’s happening: Remove bonus and update score
  • Case 4 – Object 1 is a enemy and Object 2 is an enemyHitBar
    What’s happening: Remove enemy and update lives
  • Case 5 – Object 1 is a enemyHitBar and Object 2 is an enemy
    What’s happening: Remove enemy and update lives
function onCollision( event )

  if(event.object1.name == "bullet" and event.object2.name == "enemy") then

    display.remove(event.object2)

    playerScore = playerScore + 1

  elseif(event.object1.name == "enemy" and event.object2.name == "bullet") then

    display.remove(event.object1)

    playerScore = playerScore + 1

  elseif(event.object1.name == "ship" and event.object2.name == "bonus") then

    display.remove(event.object2)

    playerScore = playerScore + 5

  elseif(event.object1.name == "enemy" and event.object2.name == "enemyHitBar") then

    display.remove(event.object1)

    updateLives()

  elseif(event.object1.name == "enemyHitBar" and event.object2.name == "enemy") then

    display.remove(event.object2)

    updateLives()

  end

  gui_score.text = "Score: " .. playerScore

  gui_score.x = screenW

end

15. Syncing Movement Timers

Since we have everything set up for our game, we just need to make everything move! Inside of the function scene:enterScene() – remember that the enterScene function is outside of the createScene function – we will create 5 timers and one runtime listener. The timers will send out the bullets, enemies, and bonuses while the runtime listener will handle the collision detection.

function scene:enterScene( event )

  local group = self.view

  tmr_fireShip = timer.performWithDelay(bulletSpawn, fireShip, 0)

  tmr_sendSlowEnemies = timer.performWithDelay(slowEnemySpawn, createSlowEnemy, 0)

  tmr_sendSlowEnemies2 = timer.performWithDelay(slowEnemySpawn+(slowEnemySpawn*0.5), createSlowEnemy, 0)

  tmr_sendFastEnemies = timer.performWithDelay(fastEnemySpawn, createFastEnemy, 0)

  tmr_sendBonus = timer.performWithDelay(2500, createBonus, 0)

  Runtime:addEventListener( "collision", onCollision )

end

16. Destroying the Scene

The final addition (I promise!) is the scene:destroyScene() function and the scene event listeners. The destroy scene function will make sure the physics are removed once the player leaves the scene. The scene event listeners will call the createScene, enterScene, and destroyScene respectively.

function scene:destroyScene( event )

  local group = self.view

  package.loaded[physics] = nil

  physics = nil

end

scene:addEventListener( "createScene", scene )

scene:addEventListener( "enterScene", scene )

scene:addEventListener( "destroyScene", scene )

return scene

Conclusion

Congratulations! You have learned about a lot of things such as Corona’s storyboard feature, physics, collisions, and so much more! These are valuable skills that can be applied to almost any game and if you want to build this game for your device, I strongly recommend the official Corona documents on building for the device.

Thank you so much for reading! If you have any questions, please leave them in the comments below.

Advertisement