How to Make a Game Like Jetpack Joyride using LevelHelper and SpriteHelper [Corona Edition] – Part 3

Bogdan Vladu

This is a post by special contributor Bogdan Vladu, an iOS application developer and aspiring game developer living in Bucharest, Romania.

Create a game like Jetpack Joyride with LevelHelper and SpriteHelper!

Create a game like Jetpack Joyride with LevelHelper and SpriteHelper!

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 a mouse that can use his jetpack to fly through a scrolling level, complete with animations and endless scrolling. Check out how we did it in Part One and Part Two.

I hope you enjoyed the first two parts, but, psst… Part Three is where we get to the really fun stuff!

In this part of the series, we’ll make our game fully capable of handling collisions.

In other words, by the end of this part we’ll be able to reward and kill the mouse! :]

We’ll also add sounds, more sophisticated animations, and we’ll iron-out some problems with game play.

So what are you waiting for – time to give our mouse the ride of his life!

Getting Started

To continue with this part of the tutorial series, first make sure that you have the complete project from Part Two, available here.

You will also need to download this sound pack, which we will be using later on.

To open our project in LevelHelper, navigate to your last project folder in Finder and double click on the file RocketMouse.lhproject.

LevelHelper should open the last level we were working on. If for some reason LevelHelper opens an earlier version of the level, just go to the Levels section to find and open the latest version.

Implementing Collisions: Overview

Our game has a flying mouse and shooting lasers (with the help of animations), and it also has coins for the player to collect. However, if you run into the coins or lasers nothing happens – so it’s time to add some gameplay for that!

Before we do so, however, let’s take a look at how things are currently set up.

In LevelHelper, right click the coin in the right sidebar (first tab) and select Open SpriteHelper Scene. Select the coin in SpriteHelper, and you’ll see that in the physics section, we have the “Is Sensor” option checked:

Settings for the coin in SpriteHelper has Is Sensor set to yes

We have this checked because we want the player to trigger a collision response when it touches a coin, so that we know to give points to the user, but we don’t want the mouse to behave like it’s colliding with the coin.

What about the laser? If we look at the laser property, this sprite does not have the “Is Sensor” option selected:

Settings for the laser does not have is sensor checked

Why? Don’t we want the same behavior? We do, but our need to keep track of our lasers’ animation requires us to to handle them differently.

If a sprite has Is Sensor selected, the collision will be triggered only when the mouse first touches the sprite. But for our lasers, we need the possibility of continuous collisions (every frame), because from one frame to another the animation of the laser might change (from off to on, for example) while the player is still in contact with the laser.

If we have Is Sensor enabled for the lasers, and the player makes contact with a laser when the laser is off, we have a collision response – but we wouldn’t fry the mouse, because after all, the laser is off.

But then the laser turns on. But because the collision already has been performed, we have no way of knowing if the player is still touching the laser.

How can we solve this? Well, it’s easy. Using LevelHelper’s collisions behavior together with Box2d, we can disable the collision response so that the mouse will move over the laser without behaving like it’s colliding with it. This way we will have collision trigger on every frame, and we’ll know if we need to kill the player.

Implementing Collisions: Coins

OK, finally time to code! Open up your main.lua file and declare this new method at the top of the file:

function setupCollisionHandling()
 
	loader:useLevelHelperCollisionHandling();
	loader:registerBeginOrEndCollisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.COIN, mouseCoinCollision);
 
end

Next, call this new method after we have loaded the level:

local playerVelocity = 0;
local playerWasFlying = false;
local playerShouldFly = false;
 
setupCollisionHandling()

So what have we done here?

In the setupCollisionHandling method, we first tell the LevelHelperLoader instance that we want to use its collision handling and not create our own collision handling. My advice is: always use this, because its fast, easy and painless.

On the second call, we register a method that we want LevelHelper to call whenever a collision between a sprite with the tag PLAYER (in our case the mouse sprite) and a sprite with the tag COIN (in our case any coin sprite) happens.

Remember how we set up collision tags for these in part two? Well LevelHelper automatically generates constants for these so we can use them in code! You can go to LevelHelperLoader.lua if you’re curious where they are defined and look at the top of the file.

LevelHelper collision handling has multiple types of collision but here we use beginOrEnd because our coin sprites are defined as sensors and Box2d handles collision for sensor objects only on “begin” collision types.

Now let’s define the methods that will be used for collision responses between the mouse and coin. Add this new method right before setupCollisionHandling. Note it is very important that the callback methods are defined beforehand, or else you are passing a nil value to LevelHelper and collision will not work!

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

As you can see, this method gets an event object as an argument. This is the Corona object that will give us info about the collision.

So how do we get the coin sprite from this collision? Well, we register the coin sprite as being the second tag (tag B) in a call to registerBeginOrEndCollisionCallbackBetweenTags. So if second tag is the coin, then we take the sprite using event.spriteB;.

If you want to learn more about the collisions with LevelHelper, check out the official LevelHelper documentation.

Next, we make sure the coin is not nil. This is not strictly necessary, but it’s a good way to avoid errors. As a general note, it’s always good to check against nil.

If the coin is visible, we call a method that we’ll write in a minute to give points to the user for taking the coin. We then make the coin invisible in order to hide it from the screen and give visual feedback to the user that he now has the coin in his virtual wallet.

Compiling now will give an error, because we did not define the scoreHitAtPosition method. Let’s define it now. Put the following before the mouseCoinCollision method:

local score = 0; --our global score value
function scoreHitAtPosition(position, points)  
    score = score + points;
    print("Score: " .. tostring(score))
end

We also declared a global variable “score” that will keep track of our virtual money.

Compile and run the game, and you’ll see that when the mouse collides with the coins, the coins disappear!
Comment this line if you don’t want to see the Box2d physical objects:

physics.setDrawMode( "hybrid" )

Collecting coins in our game

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.

Gratuitous Sound Effects

If you’ve read game tutorials on this blog before, you know we’d never leave you hanging without some gratuitous (and awesome) sound effects! :]

Navigate to your project folder in Finder. Then, open a new Finder window and navigate to where you saved the sounds pack that you downloaded at the beginning of this tutorial.

Inside the project folder, create a new folder called Music, and drag all the sound files from the sound pack into this new folder.

Now that we’ve added our sound files to our project, let’s code the sound so we can make some noise!

At the top of gameLogic.lua declare a couple of global variables:

local backgroundMusic = nil;
local laserSound = nil;
local coinSound = nil;
local groundSound = nil;
local bunnyHitSound = nil;
local flySound = nil;
local hitObjectSound = nil;
local loseSound = nil;

Then inside the “enterScene” method load the sounds as follows:

backgroundMusic = audio.loadStream("Music/backgroundMusic.m4a")
laserSound = audio.loadSound("Music/laser.wav")
coinSound = audio.loadSound("Music/coin.wav")
 
groundSound = audio.loadSound("Music/ground.wav")
bunnyHitSound = audio.loadSound("Music/bunnyHit.wav")
flySound = audio.loadSound("Music/fly.wav")
hitObjectSound = audio.loadSound("Music/hitObject.wav")
loseSound = audio.loadSound("Music/lose.wav")
 
audio.play( backgroundMusic, { channel=1, loops=-1, fadein=5000 })

Here we also start the background music playback and we set it as looping.

Now inside mouseLaserCollision(event) method add the following line just before killPlayer()

if(laser:activeFrame() ~= 1)then
	    audio.play( laserSound ) --ADD THIS LINE
	    killPlayer();
    end

Then inside killPlayer() add the following line right at the end of the method:

audio.play( loseSound )

Inside “mouseCoinCollision(event)” method add the following right after scoreHitAtPosition call.

 if(coin.alpha == 1.0)then
	        scoreHitAtPosition({x = coin.x, y = coin.y}, 100);
	        audio.play( coinSound ) --ADD THIS LINE
        end

Inside onEnterFrame method add the sound right before the “if(playerVelocity > 1.5)then” line:

audio.play( flySound )

Compile and run the game, and enjoy the new music and sound effects! :]

The complete project up to this point can be downloaded here.

Collisions Between Mouse, Cats and Dogs

So far our mouse can die if he hits a laser, but he’s getting off to easy when it comes to the cats and dogs! So let’s add some collision handling for them as well.

Add the following at the end of setupCollsionHandling method:

loader:registerPreColisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.DOG, mouseDogCatCollision);
loader:registerPreColisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.CAT, mouseDogCatCollision);

Let’s now define the method for the callback. Because our game will perform the same action for collisions with cats and dogs, we only need one method. Add this somewhere before the setupCollisionHandling method:

function mouseDogCatCollision(event)  
 
	audio.play( hitObjectSound );
 
	loader:cancelPreCollisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.DOG);
 
	loader:cancelPreCollisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.CAT);
 
	killPlayer();
end

Here we play the needed sound, then we cancel the callback between the player and the cat or dog, because we no longer need them. Then we kill the player.

Compile and run the game, and now your mouse can die when he hits a cat or dog too – so watch out! :]

Mouse dies from hitting a dog

Tweaking Gameplay

Our game is looking good so far, but there are still a number of problems.

In my level there are places where the player can’t get past a laser because it’s active, but he can’t go under it because there’s a cat in the way. Your level may have similar problems.

It’s time to play-test the level and make any necessary modifications to be sure it’s possible for the player to successfully finish the game.

Open up LevelHelper with our level03 level, and drag the cats, dogs and laser sprites as necessary so that the player has a way through. (But don’t make it too easy!)

Make sure to save the level!

Since we’re modifying the level anyway, let’s create a way to know when the player touches the ground. To do this we’ll define a new tag and add that tag to the bottom border of the physic boundary. Click the Define Tag button (as we did in Part Two) and add the tag “GROUND.”

Now that we have this new tag, let’s assign it to the bottom part of the Physic Boundary shape.
To do this click the Physic Boundaries button.

Save your level in LevelHelper.

We still have to define the collision callback between the player and the ground. Add the following at the end of setupCollisionHandling method:

loader:registerPreColisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.GROUND, mouseGroundCollision);

Define the mouseGroundCollision method somewhere above the setupCollisionHandling method, as follows:

--------------------------------------------------------------------------------
function mouseGroundCollision(event)
 
    if(playerIsDead == true)then
        return;
    end
 
    if(playerWasFlying == true)then
 
    	audio.play(groundSound);
 
 		player:startAnimationWithUniqueName("mouseFall");
 
		local function onAnimationEnded( event ) 
	 		if(event.phase == "end")then
	 			player:startAnimationWithUniqueName("mouseRun")
	 			player:removeEventListener("sprite", onAnimationEnded);
			end
		end
		player:addEventListener("sprite", onAnimationEnded)
 
    end
    playerWasFlying = false;
end
--------------------------------------------------------------------------------

We test if the player is dead so we can return if it is. We then test if the player was flying when they touched the ground. If they were, we play landing sound.

Then we start the mouseFall animation and add a notification on the sprite. That way when the animation ends, we get notified so that we can set another animation on the player sprite. Since we run a new animation on the mouse we need to register for that animation.
Add the following where you register the animations.

animationMgr:registerAnimationWithNameOnSpriteWithName("mouseFall", "player");

We also set the animation “mouseRun”. Why are we not registering for it? Because this animation is set by default in LevelHelper and its already registered.

Playing the game now, it looks like we have almost everything in place. But there is still one thing we need to fix!

You may have noticed that if we run the game now, coins taken from the level don’t reappear when the parallax restarts. We hid them, remember, after player collisions, so we need to add a way to make them visible again when the parallax starts over.

Inside the enterScene method declaration, add the following line, after you’ve taken reference to the parallax node:

....rest of the code
parallaxNode = loader:parallaxNodeWithUniqueName("Parallax_1")
if(nil == parallaxNode)then  print("Could not find parallax node.") end
 
parallaxNode:registerSpriteHasMovedToEndListener(spriteInParallaxHasReset) --ADD THIS LINE
 
...rest of the code

This new method registers on the parallax another new method (spriteInParallaxHasReset) that will be called whenever a sprite in the parallax is reset to the back of the parallax. This method gets called when the sprite exits the view.

Define spriteInParallaxHasReset before the enterScene declaration:

function spriteInParallaxHasReset(sprite)
    if(LevelHelper_TAG.COIN == sprite.lhTag)then
		sprite.alpha = 1;
    end
end

Here we test the tag of the sprite that was reset by the parallax, and if it’s tagged as a COIN we make it visible again.

Compile and run your game, and see how long you can play without dying! :]

Rocket mouse part 3 complete!

Where to Go From Here?

If you’ve followed along this far, you should have a smoothly-functioning game that is very similar to Jetpack Joyride! It’s not quite complete (stay tuned for Part Four), but very close, with animations, collisions, sounds, and correct gameplay.

In case you need it, here is an example project that includes all of the code for this tutorial series up to this point.

In the fourth and final part of this tutorial series, we’ll increase the difficulty of the game by making some of the lasers rotate. But we’ll also add another point-scoring opportunity in the form of bunnies running through the level that the player can kill, and display the score as it increases.

Finally, as the crowning touch for our game, we’ll create a second scene in SpriteHelper and add it to our level as a background, so that the user will be able to look through the windows at the “outside” world.

Until then, feel free to participate in forum discussion both below and on the LevelHelper site.


This is a post by special contributor Bogdan Vladu, an iOS application developer and aspiring game developer living in Bucharest, Romania.

User Comments

8 Comments

  • After having made Levelhelper generate code I get the following error when I run the simulator:

    Runtime error
    .../mp/Documents/Corona/RocketMouse/LHAnimationNode.lua:216: attempt to call field 'newImageSheet' (a nil value)
    stack traceback:
    [C]: in function 'newImageSheet'
    .../mp/Documents/Corona/RocketMouse/LHAnimationNode.lua:216: in function 'spriteFromAnimationsNodesAndSpriteInfo'
    ...p/Documents/Corona/RocketMouse/LevelHelperLoader.lua:1055: in function 'spriteFromDictionary'
    ...p/Documents/Corona/RocketMouse/LevelHelperLoader.lua:981: in function 'createSpriteFromDictionary'
    ...p/Documents/Corona/RocketMouse/LevelHelperLoader.lua:942: in function 'createSprites'
    ...p/Documents/Corona/RocketMouse/LevelHelperLoader.lua:160: in function 'instantiateObjects'
    /Users/mp/Documents/Corona/RocketMouse/main.lua:14: in main chunk
    Runtime error: .../mp/Documents/Corona/RocketMouse/LHAnimationNode.lua:216: attempt to call field 'newImageSheet' (a nil value)

    I can see that the generated code is different in your code. I use level helper 1.4.7. and Corona 2011.704A.
    Looking forward to your answer.
    Camilla
  • You are using code that is for the daily build of corona. YOu need to revert to revision code 3 inside LevelHelper and regenerate.
    vladubogdan
  • Hi Bogdan,

    Thank you very much again for the great tutorial.

    I have some troubles on laser/player collision detection. I used following code from tutorial:

    local function mouseLaserCollision(event)
    ...
    if(laser:activeFrame() ~= 1)then
    killPlayer();
    end
    ...
    end

    When I run it, I received following error in the console:

    ...ojects/RocketMouse/LevelHelper/LevelHelperLoader.lua:933: in function <...ojects/RocketMouse/LevelHelper/LevelHelperLoader.lua:887>
    ?: in function <?:229>
    entering mouseLaserCollision(event)
    Runtime error
    ...eng/Desktop/CoronaProjects/RocketMouse/gameLogic.lua:116: attempt to call method 'activeFrame' (a nil value)
    stack traceback:


    When I did was that I dragged a laser from Asset window, tagged it with tag 'Laser', then selected 'laser in Animation Properties.
    Am I missing something?

    p.s. I am using LevelHelp 1.4.956 and SpriteHelper 1.8.153

    Thanks again for your help.

    KC
    jworld3
  • now its

    laser:currentAnimationFrame()

    i will finish the documentation for 1.4.9 this week. Sorry for this
    vladubogdan
  • Hi Bogdan,

    Thanks for the great tutorial.
    I'm running into trouble with displaying of 'Game Over' and 'Restart' text.
    Somehow these 2 text are not visible after mouse died. But I can tell they are there on the screen since I am able to click on and game restarts. Any ideas what I did wrong?

    here are my settings:

    --place following at the top of the gameLogic.lua:
    local globalGroup = nil;

    -- replaced
    local group = self.view
    with
    globalGroup = self.view
    in
    scene:enterScene( event )
    jworld3
  • I think the text is behind everything in the level.

    Now to take the global group of LH you do like this

    local lhMainGroup = loader:layerWithUniqueName("MAIN_LAYER");

    this group will contain all the level. So you must add your objects (Text, sprites) before or after this group.
    vladubogdan
  • Now that the function 'player:startAnimationWithUniqueName("mouseDie")' is no longer valid, what function do we use to switch to a new animation?

    - Jim
    NoOneSpecial
  • Hello,

    Can you post an updated version of the sample code. I downloaded the code and was trying to run it but it wasn't running. It doesn't work with the recent builds.

    Please update and post it
    Waiting to learn from here :D
    Thanks
    Bucks

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Our Books

Our Team

Tutorial Team

  • Jack

... 50 total!

Update Team

... 15 total!

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Marina Mukhina
  • Myeong Hoon
  • Jiyeon Seo

... 33 total!

Subject Matter Experts

... 4 total!