How to Make a Game Like Jetpack Joyride using LevelHelper and SpriteHelper [Corona Edition] – Part 3
This is a post by special contributor Bogdan Vladu, an iOS application developer and aspiring game developer living in Bucharest, Romania. Welcome back to our Jetpack Joyride tutorial series! In this tutorial series, we are making a game similar to Jetpack Joyride using Corona SDK, and the LevelHelper and SpriteHelper tools. So far, we’ve got […] By .
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Make a Game Like Jetpack Joyride using LevelHelper and SpriteHelper [Corona Edition] – Part 3
35 mins
Implementing Collisions: Lasers
The next step is to handle the collision between the lasers and the mouse.
Inside the setupCollisionHandling method, add the following at the end:
loader:registerPreColisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.LASER, mouseLaserCollision);
Here we’ve registered a pre collision callback between the player and the lasers. We did this because the laser sprites are not sensors and we want to receive notification about this collision on every frame in order to kill the mouse when the laser has become active.
Now we need to define the mouseLaserCollision method. Put the following after the mouseCoinCollision:
local lasersThatNeedHandling = {};
local function mouseLaserCollision(event)
local laser = event.spriteB;
if(playerIsDead == true)then
return;
end
if(laser:activeFrame() ~= 1)then
killPlayer();
end
-- If we make the laser a sensor, the callback will be called only once - at first collision.
-- This is not good as we want to kill the player when the laser changes to active.
-- So we make the laser sensor, so that the player and laser don't collide, durring this collision.
-- Then we keep track of the lasers that needs to change back to not being sensors
-- and in the enterFrame we set back the lasers to isSensor = false
laser.isSensor = true;
lasersThatNeedHandling[#lasersThatNeedHandling+1] = laser;
end
In the above code, we take the sprite B from the contact info. In our case sprite B is the laser. We check if the player is dead. If so, we do nothing.
Then we take the current frame from the sprite, because we need to test if the laser is active and kill the player if it is.
We test if the frame number is not 1. If it is 1, then the laser is not on, so we don’t need to kill the player. If it’s not 1, it means we need to kill the player.
We then take the laser and make it sensor so that no collision behavior will occur between player and laser.
We save the laser to a global array called “lasersThatNeedHandling” in order to set it back in the enterFrame to isSensor = false.
Lets handle the lasers and set them back to no sensors. At the end of OnEnterFrame method declared in part 2 of the tutorial add this:
for i=1, #lasersThatNeedHandling do
lasersThatNeedHandling[i].isSensor = false;
end
lasersThatNeedHandling = {};
This will iterate through all the lasers we set as sensor and make them not sensors. Then it will empty the list of the lasers that needs handling.
Notice we’re using two things that we have not yet defined: the killPlayer method and the playerIsDead variable. So let’s define them.
Before mouseLaserCollision, put the following in the code:
local playerIsDead = false;
local function killPlayer()
playerVelocity = 0.0;
playerShouldFly = false;
playerIsDead = true;
playerWasFlying = false;
rocketFlame.alpha = 0;
player:startAnimationWithUniqueName("mouseDie")
parallaxNode:setSpeed(0)
end
In the killPlayer method above, we set the velocity of the player to 0. If the player was flying, it will now fall to the ground.
We then set the playerIsDead variable to true so we know the player is dead in the methods where this information is a factor. We hide the rocket flame.
Finally, we start the death animation on the player sprite by using a LevelHelper method that will take as arguments the animation’s unique name and the sprite on which we want to perform the animation. We can take the animation name from the Animations section inside LevelHelper.
For more details on using animations with LevelHelper, check out the official LevelHelper documentation.
After that, we stop the parallax from moving by setting its speed to 0.
But if you run this now, you will have some issues. This is because we are using a few global variables like player, playerVelocity and almost all variables from inside this new method. Why the issues? Its because we are declaring this variables after we create this method and for Lua this variables while they are declared later in the code – in the interior of this method they will be local variable, not global so they will be nil.
We need to move the variables at the top of the file before this new method. So at the top of the file add this:
local parallaxNode = nil
local player = nil
local rocketFlame = nil
local playerVelocity = 0;
local playerWasFlying = false;
local playerShouldFly = false;
Then change the code to initialize this variables by doing this:
parallaxNode = loader:parallaxNodeWithUniqueName("Parallax_1")
if(nil == parallaxNode)then print("Could not find parallax node.") end
player = loader:spriteWithUniqueName("player")
if(nil == player)then print("Could not find player.") end
rocketFlame = loader:spriteWithUniqueName("flame")
if(nil == rocketFlame)then print("Could not find rocket flame.") end
rocketFlame.alpha = 0 --You can do it in LH, but I do it here so you guys can see it in LH
Notice we removed the local flag before every variables.
Since inside killPlayer() we are using a new animation called “mouseDie” lets register that animation on the player (mouse). Add this where you register the animations:
animationMgr:registerAnimationWithNameOnSpriteWithName("mouseDie", "player");
Because we need a way to restart the game when the player dies, we will update the game to use the new Storyboard API.
So if you reach this point you can download everything until this point from here.
Update project to use Storyboard API
The first thing we need to do is rename the file main.lua to gameLogic.lua.
After you do this, create a new main.lua file, and inside put the following code:
display.setStatusBar( display.HiddenStatusBar )
local storyboard = require "storyboard"
storyboard.gotoScene( "gameLogic" )
Next create a new file that we will be using later – call it “reset.lua” Inside reset.lua put the next code:
local storyboard = require( "storyboard" )
local scene = storyboard.newScene()
function scene:enterScene( event )
local group = self.view
storyboard.purgeScene( "gameLogic" )
storyboard.gotoScene( "gameLogic", "fade", 250 )
end
-- "enterScene" event is dispatched whenever scene transition has finished
scene:addEventListener( "enterScene", scene )
return scene
Because in our game we don’t use some of the events from Storyboard API like createScene, exitScene, destroyScene, I made the code cleaner and did not included this events, but if you need them you should include them.
Now its time to update gameLogic.lua (our old main.lua file). This will be the hardest part, but I’m confident we will get over this quickly.
The first thing we need to do is, add the following lines at the very top of the file:
local storyboard = require( "storyboard" )
local scene = storyboard.newScene()
Next move all the global variables after the storyboard.newScene.
local parallaxNode = nil
local player = nil
local rocketFlame = nil
local playerVelocity = 0;
local playerWasFlying = false;
local playerShouldFly = false;
Move all the methods we previously defined at the top, after the global variables.
--------------------------------------------------------------------------------
local score = 0; --our global score value
function scoreHitAtPosition(position, points)
score = score + points;
print("Score: " .. tostring(score))
end
--------------------------------------------------------------------------------
local function mouseCoinCollision(event)
local coin = event.spriteB;
if(nil ~= coin)then
if(coin.alpha == 1.0)then
scoreHitAtPosition({x = coin.x, y = coin.y}, 100);
end
coin.alpha = 0.0;
end
end
--------------------------------------------------------------------------------
local playerIsDead = false;
local function killPlayer()
playerVelocity = 0.0;
playerShouldFly = false;
playerIsDead = true;
playerWasFlying = false;
rocketFlame.alpha = 0;
player:startAnimationWithUniqueName("mouseDie")
parallaxNode:setSpeed(0)
end
--------------------------------------------------------------------------------
local lasersThatNeedHandling = {};
local function mouseLaserCollision(event)
local laser = event.spriteB;
if(playerIsDead == true)then
return;
end
if(laser:activeFrame() ~= 1)then
killPlayer();
end
-- If we make the laser a sensor, the callback will be called only once - at first collision.
-- This is not good as we want to kill the player when the laser changes to active.
-- So we make the laser sensor, so that the player and laser don't collide, durring this collision.
-- Then we keep track of the lasers that needs to change back to not being sensors
-- and in the enterFrame we set back the lasers to isSensor = false
laser.isSensor = true;
lasersThatNeedHandling[#lasersThatNeedHandling+1] = laser;
end
--------------------------------------------------------------------------------
function setupCollisionHandling()
loader:useLevelHelperCollisionHandling();
loader:registerBeginOrEndCollisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.COIN, mouseCoinCollision);
loader:registerPreColisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.LASER, mouseLaserCollision);
end
--------------------------------------------------------------------------------
function startPlayerFly()
playerVelocity = 0.5;
playerShouldFly = true;
rocketFlame.alpha = 1;
player:startAnimationWithUniqueName("mouseFly")
end
--------------------------------------------------------------------------------
function cancelPlayerFly()
playerShouldFly = false;
rocketFlame.alpha = 0;
playerWasFlying = true;
playerVelocity = 0.0;
end
--------------------------------------------------------------------------------
local onTouch = function( event )
if(event.phase == "began")then
startPlayerFly()
elseif (event.phase == "ended" or event.phase == "cancelled")then
cancelPlayerFly()
end
end
--------------------------------------------------------------------------------
local onEnterFrame = function( event )
if(playerShouldFly == true)then
player:applyLinearImpulse(0, -playerVelocity, player.x, player.y);
playerVelocity = playerVelocity + 0.01;
if(playerVelocity > 1.5)then
playerVelocity = 1.5;
end
end
for i=1, #lasersThatNeedHandling do
lasersThatNeedHandling[i].isSensor = false;
end
lasersThatNeedHandling = nil
lasersThatNeedHandling = {};
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
We will now declare the StoryBoard “enterScene” event. In this event we will load the physics engine, the level file, we will take the reference to our sprites and we will register for events.
function scene:enterScene( event )
local group = self.view
local ui = require ("ui")
local physics = require("physics")
local fps = require("fps");
physics.start()
--comment next line to disable debug drawing
physics.setDrawMode( "hybrid" )
display.setStatusBar( display.HiddenStatusBar )
physics.setGravity( 0, 10 );
parallaxNode = nil
player = nil
rocketFlame = nil
playerVelocity = 0;
playerIsDead = false;
playerWasFlying = false;
playerShouldFly = false;
lasersThatNeedHandling = nil
lasersThatNeedHandling = {};
score = 0;
require "LevelHelperLoader"
local animationMgr = LHAnimationsMgr:sharedInstance();
animationMgr:registerAnimationWithNameOnSpriteWithName("mouseFly", "player");
animationMgr:registerAnimationWithNameOnSpriteWithName("mouseDie", "player");
application.LevelHelperSettings.directorGroup = self.view;
loader = LevelHelperLoader:initWithContentOfFile("level03.plhs")
loader:instantiateObjects(physics)
loader:createPhysicBoundaries(physics)
parallaxNode = loader:parallaxNodeWithUniqueName("Parallax_1")
if(nil == parallaxNode)then print("Could not find parallax node.") end
player = loader:spriteWithUniqueName("player")
if(nil == player)then print("Could not find player.") end
rocketFlame = loader:spriteWithUniqueName("flame")
if(nil == rocketFlame)then print("Could not find rocket flame.") end
rocketFlame.alpha = 0 --You can do it in LH, but I do it here so you guys can see it in LH
setupCollisionHandling()
Runtime:addEventListener( "touch", onTouch )
Runtime:addEventListener( "enterFrame", onEnterFrame )
end
Notice how we also give a default value to our global variables?
parallaxNode = nil
player = nil
rocketFlame = nil
playerVelocity = 0;
playerIsDead = false;
playerWasFlying = false;
playerShouldFly = false;
lasersThatNeedHandling = nil
lasersThatNeedHandling = {};
score = 0;
This is because when the level restarts this values keep they’re previous values and we need to reset them.
We also loaded the level and place it inside the StoryBoard group by calling this line right before we loaded the level:
application.LevelHelperSettings.directorGroup = self.view;
The next part is easier. We declare the StoryBoard “exitScene” event where we will also remove our level from the memory, remove the touch and enter frame event and stop the physics engine.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function scene:exitScene( event )
local group = self.view
loader:removeSelf()
loader = nil;
player = nil;
Runtime:removeEventListener( "enterFrame", onEnterFrame )
Runtime:removeEventListener( "touch", onTouch )
physics.stop()
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- "enterScene" event is dispatched whenever scene transition has finished
scene:addEventListener( "enterScene", scene )
-- "exitScene" event is dispatched before next scene's transition begins
scene:addEventListener( "exitScene", scene )
return scene
But all this was so that we could restart the scene when the player dies. So lets display a restart scene text when the player dies.
Add the following at the end of the killPlayer() function:
local gameOverText = display.newText("Game Over!", 0, 0, native.systemFont, 32)
gameOverText:setTextColor(255, 0, 0)
gameOverText.x = display.viewableContentWidth / 2
gameOverText.y = display.viewableContentHeight /2 - 40
local restartText = display.newText("Restart", 0, 0, native.systemFont, 24)
restartText:setTextColor(255, 255, 255)
restartText.x = display.viewableContentWidth / 2
restartText.y = display.viewableContentHeight /2
local onRestartTouch = function( event )
if(event.phase == "began")then
local storyboard = require "storyboard"
storyboard.gotoScene( "reset" )
Runtime:removeEventListener("touch", restartText);
end
end
restartText:addEventListener( "touch", onRestartTouch )
globalGroup:insert(gameOverText)
globalGroup:insert(restartText)
Here we create 2 text objects. We then set a touch event on the “Restart” text and insert the text objects in the global group which in our case will be the Storyboard group. In the restart text touch method we restart the scene by calling our “reset” scene and we remove the touch listener that was set on the text.
If you run now you will get an error because globalGroup was not defined.
Add the following at the beginning of our gameLogic.lua file where we defined all the other global variables:
local globalGroup = nil;
Now inside enterScene(event) method replace the local group = self.view to be:
function scene:enterScene( event )
globalGroup = self.view
......
If you run the game now, everything should be just fine. But if you die and press in another place then “Restart” button you will see that the player gets resurrected from the dead and starts flying again. Lets fix that.
Inside startPlayerFly() method add the following right at the top:
function startPlayerFly()
if(playerIsDead == true)then
return
end
......
You can download the full project up until this point from here.