Create a Simple Space Shooter with Corona SDK

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:

  • Creation of Game controls
  • Spawning of enemy ships
  • Score and wave system
  • Power ups which are dropping from the sky
  • Checking for Collisions
  • Remove off screen objects
  • Sound effects and game music

    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

    DOWNLOAD SOURCE FILES

  • Leave a Reply

    Your email address will not be published.

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

    *