In this tutorial we build a simple space shooter by using the Corona SDK framework. By the end of this post you will find a download link for the complete source code. Feel free to grab it and use it for your own game developments.
In the video below you can see how this simple game works.
The following features are implemented:
If you want to get a better understanding of some of the basics of the Corona SDK; this tutorial might be useful for you. You are probably not developing a space shooter, but many of the concepts can be reused in other games.
Before we start, when you try to run some of the code while following along you will notice it will not work. This is because the last code block on the bottom of this post is required to run the game. So perhaps you can better first implement the last piece of code called startGame (also don’t forget to call startGame() ).
First we will set up our global variables, a display group, global functions, load the audio and set up the physics engine of the Corona SDK. Some small notes are that we have set the gravity to zero. This means in this game I won’t apply any gravity to the objects. Also I created a group for all spawned enemies. In case you have your audio files in a separate folder remember to reference the correct folder in the audio loading.
The variables and forward references might not be clear to you at this point, but I hope that each of these items will become more clear throughout this tutorial.
-- general stuff display.setStatusBar(display.HiddenStatusBar) local cWidth = display.contentCenterX local cHeight = display.contentCenterY -- physics local physics = require("physics") physics.start() physics.setGravity( 0,0) -- groups local enemies = display.newGroup() -- global variables local gameActive = true local waveProgress = 1 local numHit = 0 local shipMoveX = 0 local ship local speed = 6 local shootbtn local numEnemy = 0 local enemyArray = {} local onCollision local score = 0 local gameovertxt local numBullets = 3 local ammo local AmmoActive = true -- global functions local removeEnemies local createGame local createEnemy local shoot local createShip local newGame local gameOver local nextWave local checkforProgress local createAmmo local setAmmoOn local backgroundMusic --- audio local shot = audio.loadSound ("laserbeam.mp3") local wavesnd = audio.loadSound ("wave.mp3") local ammosnd = audio.loadSound ("ammo.mp3") local backgroundsnd = audio.loadStream ( "musicbackground.mp3")
In the next step we will set the background of the game, the score, wave and bullet text, and we set the controls for the game. The score text will display the score of the user based on the shot enemies. The textWave well let the user know what his/her progress is in the game, and finally the textBullets displays the remaining number of bullets.
Next up, we position the left and right control images and the button to fire a bullet (or actually a laser beam). in moveShip, leftArrowtouch and rightArrowtouch we set the functions what need to happen when the user clicks the right or left direction controls. Speed is used as a variable (set as 6) to determine how fast the ship moves to the left or right. You can lower or increase the speed by changing this variable in your global variables above. Finally, we create some virtual walls in the game to avoid that the user move the ship off the screen (into the larger universe ;) ).
-- background local background = display.newImage("spacebackground.png") background.x = cWidth background.y = cHeight -- score textScore = display.newText("Score: "..score, 10, 10, nil, 12) textWave = display.newText ("Wave: "..waveProgress, 10, 30, nil, 12) textBullets = display.newText ("Bullets: "..numBullets, 10, 50, nil, 12) -- set gamepad of the game local leftArrow = display.newImage("left.png") leftArrow.x = 30 leftArrow.y = display.contentHeight - 30 local rightArrow = display.newImage("right.png") rightArrow.x = 80 rightArrow.y =display.contentHeight -30 -- create shootbutton shootbtn = display.newImage("shootbutton.png") shootbtn.x = display.contentWidth -33 shootbtn.y = display.contentHeight -33 -- create gamepad local function stopShip(event) if event.phase == "ended" then shipMoveX = 0 end end local function moveShip(event) ship.x = ship.x + shipMoveX end function leftArrowtouch() shipMoveX = - speed end function rightArrowtouch() shipMoveX = speed end local function createWalls(event) if ship.x < 0 then ship.x = 0 end if ship.x > display.contentWidth then ship.x = display.contentWidth end end
Now that we have the controls and the shoot button, we still need to create the space ship. Pretty simple :), We create the ship based on an image ship.png we set the physics to static (no physics) and position the ship on the screen. Note that I also added a property to the object ship via ship.myName = “ship”. This will make it later on easier to check for the collision with the enemies or the power ups.
function createShip() ship = display.newImage ("ship.png") physics.addBody(ship, "static", {density = 1, friction = 0, bounce = 0}); ship.x = cWidth ship.y = display.contentHeight - 80 ship.myName = "ship" end
Now we can create the enemies. We will create enemies in an Array so we can easier reference to them throughout the rest of the code. To count the number of enemies created we use numEnemy = numEnemy + 1. With other words every time this function is called we add an enemy. We set each enemy in an Array called enemyArray[numEnemy] and create the object via display.newImage and apply the physics. Next we set from which location the enemy is spawned. As it would be more fun if each enemy comes from a different location we use math.random to set the X coordinate and Y coordinate for the location in which the enemy is spawned. Transition.to describes from which location to what location the enemie flies over the screen. Also here we use math.random to let the enemy ship travel to a random location on the bottom of the screen.
We use enemies:insert to add each created enemy into a group. The Group is called: “Enemies”.
function createEnemy() numEnemy = numEnemy +1 print(numEnemy) enemies:toFront() enemyArray[numEnemy] = display.newImage("enemy.png") physics.addBody ( enemyArray[numEnemy] , {density=0.5, friction=0, bounce=0}) enemyArray[numEnemy] .myName = "enemy" startlocationX = math.random (0, display.contentWidth) enemyArray[numEnemy] .x = startlocationX startlocationY = math.random (-500, -100) enemyArray[numEnemy] .y = startlocationY transition.to ( enemyArray[numEnemy] , { time = math.random (12000, 20000), x= math.random (0, display.contentWidth ), y=ship.y+500 } ) enemies:insert(enemyArray[numEnemy] ) end
In the next function we will create the power ups for your space ship. The power ups are additional ammo for your ship. These are falling down from the top and you need to catch them. If you forget to catch these power ups your space ship will run low on ammo. :)
Most of the function is similar to the creation of enemies. There is a simple addition that we will rotate the power up. Personally I thought it would give a bit more the feeling that it is a power up instead of an enemy. If you don’t like it, you can always remove these lines of code.
function createAmmo() ammo = display.newImage("ammo.png") physics.addBody ( ammo, {density=0.5, friction=0, bounce=0 }) ammo.myName = "ammo" startlocationX = math.random (0, display.contentWidth) ammo .x = startlocationX startlocationY = math.random (-500, -100) ammo .y = startlocationY transition.to ( ammo, {time = math.random (5000, 10000 ), x= math.random (0, display.contentWidth ), y=ship.y+500 } ) local function rotationAmmo () ammo.rotation = ammo.rotation + 45 end rotationTimer = timer.performWithDelay(200, rotationAmmo, -1) end
In the next function we will create the bullets or laser beams. As you can see in the global variables the ship has default 3 bullets on board (numBullets =3 ). When there are no bullets lefts, obviously the ship cannot fire anymore. This is the first IF statement below, we check first if numBullets are not equal to zero (0). When numBullets is not zero, this means the user can shoot another bullet, we subtract 1 from numBullets to update numBullets after the shot has been fired. The bullet is initially positioned exactly equal to the position of the ship. With other words, the bullet is just waiting below the ship till the user taps the fire button, then the bullets travels from the x-coordinate location of ship up till y=-100. Here we also update the remaining number of bullets (textBullets) and play the shot soundeffect (see you global variables).
function shoot(event) if (numBullets ~= 0) then numBullets = numBullets - 1 local bullet = display.newImage("bullet.png") physics.addBody(bullet, "static", {density = 1, friction = 0, bounce = 0}); bullet.x = ship.x bullet.y = ship.y bullet.myName = "bullet" textBullets.text = "Bullets "..numBullets transition.to ( bullet, { time = 1000, x = ship.x, y =-100} ) audio.play(shot) end end
Now it becomes a bit more fun, in the next pieces of code we will check if the bullet had a collision with an enemy, or if the ship had a collision with either an enemy or one of the power ups.
In the first IF statement we check if “ship” and “enemy” are together in a collision. In case this happens I tried to visualize that the ships falls down into deeper space. This is the transition.to. In 1.5 seconds time we change the size of the ship to 0.4 and the alpha to 0 (make the ship disappear). Also, we wait that this transition is completed before we show the gameover text (function setgameOver is called).
In the second IF statement we check if “Ship” and “Ammo” are together in a collision. In that case we also run a transition.to, so visualize that the power ups indeed are collected, by very quickly increasing the size of the ship and setting it back to its normal size. As the power-up provide new ammo for the ship, we need to update the numBullets value with another 3 bullets. Also don’t forget to remove the power up display object from your screen, and cancel the rotation of the power up (just nicely cancel what we have started previously). Finally, play a small sound effect that the power up has been collected.
In the last IF statement we check if “Enemy” and “Bullet” are together in a collision. When this happens we remove both object from the screen and update the score with 10 points. Also we update the number of hits. This variable we can use later to check if a wave has been completed or not.
function onCollision(event) if(event.object1.myName =="ship" and event.object2.myName =="enemy") then local function setgameOver() gameovertxt = display.newText( "Game Over", cWidth-80, cHeight-100, nil , 50 ) gameovertxt:addEventListener("tap", newGame) end -- use setgameover after transition complete to avoid that user clicks gameover before the transition is completed transition.to( ship, { time=1500, xScale = 0.4, yScale = 0.4, alpha=0, onComplete=setgameOver } ) gameActive = false removeEnemies() audio.fadeOut(backgroundsnd) audio.rewind (backgroundsnd) end if(event.object1.myName =="ship" and event.object2.myName =="ammo") then local function sizeBack() ship.xScale = 1.0 ship.yScale = 1.0 end transition.to( ship, { time=500, xScale = 1.2, yScale = 1.2, onComplete = sizeBack } ) numBullets = numBullets + 3 textBullets.text = "Bullets "..numBullets event.object2:removeSelf() event.object2.myName=nil timer.cancel(rotationTimer) audio.play(ammosnd) end if((event.object1.myName=="enemy" and event.object2.myName=="bullet") or (event.object1.myName=="bullet" and event.object2.myName=="enemy")) then event.object1:removeSelf() event.object1.myName=nil event.object2:removeSelf() event.object2.myName=nil score = score + 10 textScore.text = "Score: "..score numHit = numHit + 1 print ("numhit "..numHit) end end
If you paid attention you saw that I called a function removeEnemies when the ship collides with an enemy and the function setgameOver is called. In this case the game is over, but the game memory still holds possibly spawned enemies. Therefore we should do some housekeeping and remove these object.
function removeEnemies() for i =1, #enemyArray do if (enemyArray[i].myName ~= nil) then enemyArray[i]:removeSelf() enemyArray[i].myName = nil end end end
For the persons who really paid attention :) They saw that a function was called newGame when the user clicks on the GameOver text. In this case the game is restarted and some values are reset to its default ones. We set the score back to zero, numEnemy back to zero, the ship alpha and scales back to its normal sizes and alpha levels. The game was stopped (gameActive = false) and needs to be set to True again etc etc.
function newGame(event) display.remove(event.target) textScore.text = "Score: 0" numEnemy = 0 ship.alpha = 1 ship.xScale = 1.0 ship.yScale = 1.0 score = 0 gameActive = true waveProgress = 1 backgroundMusic() numBullets = 3 textBullets.text = "Bullets "..numBullets AmmoActive = true end
In the ammoStatus function we first check if the game is active. If so, we create an enemy. I can understand this might be a bit weird to place it here. I could have positioned it apart from the ammoStatus function but it would increase the number of lines of code. Sorry, i’m a bit lazy sometimes. Next we check if Ammo is Active, meaning can we spawn a powerup or not. If True, we check if the number of bullets left are either equal to 0, 1, 2 or 4. In case this is true we create an ammo package (or power up). AmmoActive is then set to false otherwise the program will continue to spawn power ups. I thought this would make the game too easy, so I set further down in the program that AmmoActive is set to True again only after 5 seconds.
The function checkforProgress check how the user is doing against the current wave. If the numHit are equal to the waveProgress the game is paused, a music is played and a text is displayed to start the next wave. Initially I have set the waveProgress to 1. This means after you have shot 1 enemy your first wave has been completed. Then we increment the waveProgress with 1, meaning the 2nd wave can be completed after shooting down 2 enemies etc etc. After the user clicks on the waveTxt the nextWave function is called wich sets the game active again (true) and resets the numHit back to zero. This means every time we restart counting the number of enemies hit.
function nextWave (event) display.remove(event.target) numHit = 0 gameActive = true end function setAmmoOn() AmmoActive = true end function ammoStatus() if gameActive then createEnemy() if AmmoActive then if (numBullets == 0) then createAmmo() AmmoActive = false end if (numBullets == 1) then createAmmo() AmmoActive = false end if (numBullets == 2) then createAmmo() AmmoActive = false end if (numBullets == 4) then createAmmo() AmmoActive = false end end end end local function checkforProgress() if numHit == waveProgress then gameActive = false audio.play(wavesnd) removeEnemies() waveTxt = display.newText( "Wave "..waveProgress.. " Completed", cWidth-80, cHeight-100, nil, 20 ) waveProgress = waveProgress + 1 textWave.text = "Wave: "..waveProgress print("wavenumber "..waveProgress) waveTxt:addEventListener("tap", nextWave) end
The next part is to cleanup some of the enemies which you did not shoot down, these also need to be removed so they don’t keep using the memory of the device. Only I thought it was fun to punish the player because an enemy was not shot down. So for all enemies we first check if the enemy is > display.contentHeight. This means we can check if the enemy passed the bottom of the screen. When this happens we cleanup the enemies by removing them. Next we punish the user by decreasing the score with 20 and show a warning text “Watch Out!”. After 1 second we remove this text.
-- remove enemies which are not shot for i =1, #enemyArray do if (enemyArray[i].myName ~= nil) then if(enemyArray[i].y > display.contentHeight) then enemyArray[i]:removeSelf() enemyArray[i].myName = nil score = score - 20 textScore.text = "Score: "..score warningTxt = display.newText( "Watch out!", cWidth-42, ship.y-50, nil, 12 ) local function showWarning() display.remove(warningTxt) end timer.performWithDelay(1000, showWarning) print("cleared") end end end end
Next we create a function to start the background music which will loop in infinity. I set the volume at 0.5 because the source file of the music was a bit too loud. Of course you can set it how you like it yourself.
-- play background music function backgroundMusic() audio.play (backgroundsnd, { loops = -1}) audio.setVolume(0.5, {backgroundsnd} ) end
I don’t know if you have tried to start the game at this point, but you have probably noticed it did not work yet. This is because the start of the game has not been called yet. So DONT forget the line of codes below.
In startGame we create the ship and start the background music and create all the Listeners for the controls, start the listener for the collision and loop with delay the different progress checkers. Like the status of the Ammo, reset the Ammo to true or false and check the progress of the waves. As you can see the checkforProgress is done every 300 Milliseconds, this because a user can fire quickly and we need to ensure that the wave completion text is also directly displayed after the player shot down the final enemy of a certain wave.
-- heart of the game function startGame() createShip() backgroundMusic() shootbtn:addEventListener ( "tap", shoot ) rightArrow:addEventListener ("touch", rightArrowtouch) leftArrow:addEventListener("touch", leftArrowtouch) Runtime:addEventListener("enterFrame", createWalls) Runtime:addEventListener("enterFrame", moveShip) Runtime:addEventListener("touch", stopShip) Runtime:addEventListener("collision" , onCollision) timer.performWithDelay(5000, ammoStatus,0) timer.performWithDelay ( 5000, setAmmoOn, 0 ) timer.performWithDelay(300, checkforProgress,0) end startGame()
I hope you enjoyed this simple shooter tutorial. You can download all files via the link below. Please leave a comment to share what you thought about this tutorial or in case you have any question please also use the comments below