In this tutorial, we will be creating a game called Monkey Defender using the Corona SDK! This game will serve as a great foundation for a lot of different genres include defense style games. So, let’s get started!
Project Overview
In this version of Monkey Defender, the player will have to defend the monkey by shooting monkey grenades at the incoming spaceships. Every time the player successfully hits an enemy spaceship, their score will increase by one. If they fail to hit the enemy spaceship before it reaches the monkey, they will lose one banana, or one life. When the player runs out of bananas, it’s game over!
This game was built with the Corona SDK and here are some of the things you’ll learn:
- How to utilize Storyboard
- How to use widgets
- How to rotate objects based on touch
- How to use Corona’s Collision
- How to build a full game with the Corona SDK
Tutorial Prerequisites
In order to use this tutorial, you’ll need to have the Corona SDK installed on your computer. If you do not have the SDK, head over to http://www.coronalabs.com to create a free account and download the free software.
To download the graphics that I used for the game, please download the source files attached to this post. The graphics for this game come from www.opengameart.org and www.vickiwenderlich.com. The background graphic comes from Sauer2 at Open Game Art and the rest of the graphics come Vicki Wenderlich. If you decide to publish this game with these graphics, please be sure to credit both artists.
1. Build Configuration
The first step to build our game, Monkey Defender, is to create a new file called build.settings and place it inside of your project folder. The build.settings file handles all of the build time properties inside of our app. For our game, we only need to worry about the orientation of the game, and we are only going to allow landscape mode.
settings = { orientation = { default = "landscapeRight", supported = { "landscapeRight", "landscapeLeft"} }, }
2. Runtime Configuration
Next, we will create another file called config.lua and place it within your project folder. The config.lua file handles all of our runtime configurations such as the height, width, scaling type, and frame rate. For our game, we are going to set up our app to be 320×480, to be in a letter box and to have 30 frames per second.
application = { content = { width = 320, height = 480, scale = "letterBox", fps = 30, }, }
3. Building main.lua
Now that we have our project configured, we can move forward with creating main.lua. The main.lua file is the starting point of every app built with the Corona SDK, so create a new file called main.lua and move it to your project folder. Inside of our main.lua file, we will hide the status bar, add some global variables, and use Corona’s Storyboard feature to manage our scenes.
-- hide the status bar display.setStatusBar( display.HiddenStatusBar ) -- Set up some global variables for our game screenW, screenH, halfW, halfH = display.contentWidth, display.contentHeight, display.contentWidth*0.5, display.contentHeight*0.5 -- include the Corona "storyboard" module local storyboard = require "storyboard" -- load menu screen storyboard.gotoScene( "menu" )
4. Building the Menu
With our main.lua file set up, we are going to move on to our menu. The menu for this game will be simple. It will display the title of the game and a button to start the game.
Step 1
To get started, create a new file called menu.lua and place the file in your project folder. The first addition to this file is to add a storyboard and the widget library. While the storyboard will allow us to easily manage our scene, the widget library will allow us to easily add common elements to our app. In this case, we will be using the widget library to add a button to our menu. We’ll get to that later.
local storyboard = require( "storyboard" ) local scene = storyboard.newScene() local widget = require "widget"
Step 2
After the requires, we will create our first function called scene:createScene()
. This function will be called when the scene does not exist and is a perfect place for our game title and button.
-- Called when the scene's view does not exist: function scene:createScene( event ) local group = self.view end
Step 3
Inside of our scene:createScene()
function, we will create a new display object that will be used as our background. If you haven’t already, make sure you download the source files for this tutorial and place all of the graphics inside a folder named images in your project.
The background display object will be centered on the screen and inserted into our group variable. By inserting the display object into our group variable, we are telling Corona that this object belongs to this scene. When we switch scenes, Corona will know to remove this object or hide it because we are no longer viewing the menu scene.
An in-depth look at storyboards is outside of the scope of this tutorial, but you can read more at the official documentation.
-- Insert a background into the game local background = display.newImageRect("images/background.png", 480, 320) background.x = halfW background.y = halfH group:insert(background)
Step 4
After our background object, we will place a text object that will display the title of our game. This text object will be centered on the screen and inserted into the group variable.
-- Insert the game title local gameTitle = display.newText("Space Monkey",0,0,native.systemFontBold,32) gameTitle.x = halfW gameTitle.y = halfH - 80 group:insert(gameTitle)
Step 5
Our last addition to the function scene:createScene()
will be a button widget. This widget will allow players to start the game. Before we can add the widget, we need to create a function that will handle the touch event.
When the button is touched, the following function will be called. This function will send players to the game scene, which we will be creating later.
-- This function will only be fired when the widget playBtn is touched. local function onPlayBtnRelease() storyboard.gotoScene( "game", "fade", 500 ) return true end
Step 6
After the onPlayBtnRelease
function, we will then add the button to the menu scene. We add the button by using widget.newButton
with a couple of parameters. The label property will set the text of our button and the onRelease
property will tell our app which function to fire when it’s touched. Then, we will place the button in the center of the screen and insert it into the group variable. The playBtn
will be the last piece added to the scene:createScene()
function.
-- Create a widget button that will let the player start the game local playBtn = widget.newButton { label="Play Now", onRelease = onPlayBtnRelease } playBtn.x = halfW playBtn.y = halfH group:insert( playBtn )
Step 7
Right after the scene:createScene()
function, we are going to add the scene:enterScene()
function. This function will be called after the scene is created and it’s on the screen.
function scene:enterScene(event) local group = self.view if(storyboard.getPrevious() ~= nil) then storyboard.purgeScene(storyboard.getPrevious()) storyboard.removeScene(storyboard.getPrevious()) end end
Step 8
To wrap up our menu.lua file, we will add two event listeners and return the scene variable. This is an important step because the two event listeners will fire the appropriate functions and return the scene variable to signify the end of the file.
scene:addEventListener("createScene", scene) scene:addEventListener("enterScene", scene) return scene
5. Game Logic
By now, we’ve completed the configuration, the main.lua file and the menu.lua file. Next, we are going to create the logic for our game.
Step 1
The game logic will be held inside a file called game.lua. To get started, create a new file called game.lua and place it inside your project folder. Our first addition to the new file is to require Corona’s Storyboard.
local storyboard = require( "storyboard" ) local scene = storyboard.newScene()
Step 2
Next, we will add physics to our game. We will use physics to make collision detection between the bullets and the enemy ships easier. Right after the physics capability is added, we will pause it so it doesn’t interfere with the creation of the scene.
local physics = require "physics" physics.start(); physics.pause()
Step 3
After the physics, we will predefine the variables for our game.
local background, monkey, bullet, txt_score local tmr_createBadGuy local lives = {} local badGuy = {} local badGuyCounter = 1 local score = 0
Step 4
The next set of variables will be used as settings for our game. Feel free to change the speeds of the game or increase the number of lives for the player.
local numberOfLives = 3 local bulletSpeed = 0.35 local badGuyMovementSpeed = 1500 local badGuyCreationSpeed = 1000
Step 5
Now we will create our scene:createScene
function.
-- Called when the scene's view does not exist: function scene:createScene( event ) local group = self.view
Step 6
Next, we will create a function that will be fired when the background display object is touched, and this function will contain the bulk of our game logic. When fired, we will rotate the monkey towards the touch event and fire a bullet towards the same touch event.
First, let’s create the touched function and create a conditional statement so our logic only runs during the begin phase.
function touched(event) if(event.phase == "began") then
Within the if-then statement, we use the math library to determine the angle between the monkey and the touch event. This is accomplished by using a combination of math.deg
and math.atan2
to find where the monkey needs to be rotated. After the rotation degree is found, we rotate the monkey to the appropriate position.
angle = math.deg(math.atan2((event.y-monkey.y),(event.x-monkey.x))) monkey.rotation = angle + 90
Since this function is going to fire a bullet, we have to create the bullet as a display object. Once it’s created, we will make the physics object so it can respond to collisions with the enemies.
bullet = display.newImageRect("images/grenade_red.png",12,16) bullet.x = halfW bullet.y = halfH bullet.name = "bullet" physics.addBody( bullet, "dynamic", { isSensor=true, radius=screenH*.025} ) group:insert(bullet)
Now that we have our bullet, we need to find out where to send it. To do this, we first determine whether we need to send the bullet to the left or right and then use the y = mx + b formula to determine the y location. Our last bit of math is to find the distance between the two points so we can determine how fast to project the bullet.
-- Find out if we need to fire the bullet to the left or right local farX = screenW*2 if(event.xStart >= screenW/2)then farX = screenW*2 else farX = screenW-(screenW*2) end -- Use y = mx + b to find the Y position local slope = ((event.yStart-screenH/2)/(event.xStart-screenW/2)) local yInt = event.yStart - (slope*event.xStart) local farY = (slope*farX)+yInt -- Get the distance from the bullet to bullet destination local xfactor = farX-bullet.x local yfactor = farY-bullet.y local distance = math.sqrt((xfactor*xfactor) + (yfactor*yfactor))
Now that we know the distance and the x/y coordinates of the bullet destination, we can send our bullet to the destination using transition.to
. We’ll also need to include a couple of end statements to wrap up the touched function.
bullet.trans = transition.to(bullet, { time=distance/bulletSpeed, y=farY, x=farX, onComplete=nil}) end end
Step 7
After all that math, the next few steps are simple. We will add a background to our game (we will attach an event listener to the background later), the monkey, and a text object to display the score.
-- Create a background for our game background = display.newImageRect("images/background.png", 480, 320) background.x = halfW background.y = halfH background:setFillColor( 128 ) group:insert(background) -- Place our monkey in the center of screen monkey = display.newImageRect("images/spacemonkey-01.png",30,40) monkey.x = halfW monkey.y = halfH group:insert(monkey) -- Create a text object for our score txt_score = display.newText("Score: "..score,0,0,native.systemFont,22) txt_score.x = 430 group:insert(txt_score)
We will also insert three bananas to represent three player lives in the top right of the screen.
-- Insert our lives, but show them as bananas for i=1,numberOfLives do lives[i] = display.newImageRect("images/banana.png",45,34) lives[i].x = i*40-20 lives[i].y = 18 group:insert(lives[i]) end
Step 8
Next, we will create a function that will send bad guys toward our player. We are going to call this function createBadGuy
and we will first determine which direction to send the bad guy from.
-- This function will create our bad guy function createBadGuy() -- Determine the enemies starting position local startingPosition = math.random(1,4) if(startingPosition == 1) then -- Send bad guy from left side of the screen startingX = -10 startingY = math.random(0,screenH) elseif(startingPosition == 2) then -- Send bad guy from right side of the screen startingX = screenW + 10 startingY = math.random(0,screenH) elseif(startingPosition == 3) then -- Send bad guy from the top of the screen startingX = math.random(0,screenW) startingY = -10 else -- Send bad guy from the bototm of the screen startingX = math.random(0,screenW) startingY = screenH + 10 end
After we have determined the direction that our bad guy will come from, we will then create a new display object for our bad guy and turn the bad guy into a physics object. We’ll use physics to detect collisions later on.
-- Start the bad guy according to starting position badGuy[badGuyCounter] = display.newImageRect("images/alien_1.png",34,34) badGuy[badGuyCounter].x = startingX badGuy[badGuyCounter].y = startingY physics.addBody( badGuy[badGuyCounter], "dynamic", { isSensor=true, radius=17} ) badGuy[badGuyCounter].name = "badGuy" group:insert(badGuy[badGuyCounter])
Then, we will use Corona’s transition.to
to move the bad guy towards the center of the screen. Once the transition is done and the enemy has not been hit, we will remove the bad guy and subtract one life from the player.
If the player’s lives have reached 0 or less, we will stop sending the bad guys and remove the player’s ability to send more bullets. We will also show two text objects to signify that the game is over and let the player return to the menu.
badGuy[badGuyCounter].trans = transition.to(badGuy[badGuyCounter], { time=badGuyMovementSpeed, x=halfW, y=halfH, onComplete = function (self) self.parent:remove(self); self = nil; -- Since the bad guy has reached the monkey, we will want to remove a banana display.remove(lives[numberOfLives]) numberOfLives = numberOfLives - 1 -- If the numbers of lives reaches 0 or less, it's game over! if(numberOfLives <= 0) then timer.cancel(tmr_createBadGuy) background:removeEventListener("touch", touched) local txt_gameover = display.newText("Game Over!",0,0,native.systemFont,32) txt_gameover.x = halfW txt_gameover.y = screenH * 0.3 group:insert(txt_gameover) local function onGameOverTouch(event) if(event.phase == "began") then storyboard.gotoScene("menu") end end local txt_gameover = display.newText("Return To Menu",0,0,native.systemFont,32) txt_gameover.x = halfW txt_gameover.y = screenH * 0.7 txt_gameover:addEventListener("touch",onGameOverTouch) group:insert(txt_gameover) end end; }) badGuyCounter = badGuyCounter + 1 end
Step 9
The last function inside of scene:createScene
is for collision detection. When a bullet and bad guy collide, this function will be trigged and the following will happen:
- Add 1 to the player score and update the score text object.
- Set the alpha of both objects to 0.
- Cancel the transition on each object so it does not interfere with the removal process.
- Finally, we will create a timer that will wait 1 millisecond before removing both objects. It’s always a bad idea to remove display objects in the middle of collision detection.
function onCollision( event ) if(event.object1.name == "badGuy" and event.object2.name == "bullet" or event.object1.name == "bullet" and event.object2.name == "badGuy") then -- Update the score score = score + 1 txt_score.text = "Score: "..score -- Make the objects invisible event.object1.alpha = 0 event.object2.alpha = 0 -- Cancel the transitions on the object transition.cancel(event.object1.trans) transition.cancel(event.object2.trans) -- Then remove the object after 1 cycle. Never remove display objects in the middle of collision detection. local function removeObjects() display.remove(event.object1) display.remove(event.object2) end timer.performWithDelay(1, removeObjects, 1) end end end
Step 10
After the scene:createScene
, we will then write our enter scene function. This function is fired after the scene is created and moved on to the screen. Inside the enter scene function, we will start all of the functions that we wrote in the create scene function.
function scene:enterScene( event ) local group = self.view -- Actually start the game! physics.start() -- Start sending the bad guys tmr_createBadGuy = timer.performWithDelay(badGuyCreationSpeed, createBadGuy, 0) -- Start listening for the background touch event to fire the bullets background:addEventListener("touch", touched) -- Start the listener to remove bad guys and bullets when they collide Runtime:addEventListener( "collision", onCollision ) End
Step 11
We are almost done! The last piece of our code is to add the scene event listeners and return the variable scene. The event listeners will trigger our functions and the return statement lets Corona know we are done with this scene.
scene:addEventListener( "createScene", scene ) scene:addEventListener( "enterScene", scene ) return scene
Conclusion
I hope you enjoyed this tutorial on creating a Monkey Defender game with the Corona SDK! We covered a lot of topics ranging from storyboards to geometric formulas! If you have questions or comments, please leave them below and thank you for reading.