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.
Comments