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 .

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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

Mouse dies upon colliding with a laser

You can download the full project up until this point from here.