How to Make a Game Like Jetpack Joyride using LevelHelper, SpriteHelper [Cocos2D 2.X edition] – Part 3

Bogdan Vladu
Create a game like Jetpack Joyride with latest LevelHelper and SpriteHelper!

Create a game like Jetpack Joyride with latest LevelHelper and SpriteHelper!

Welcome back to our Jetpack Joyride tutorial series! In this tutorial series, you are making a game similar to Jetpack Joyride using Cocos2D and Box2D, and the LevelHelper and SpriteHelper tools.

So far, you’ve got a mouse that can use his jetpack to fly through a scrolling level, complete with animations and endless scrolling. Check out how to do 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, you’ll make your game fully capable of handling collisions.

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

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

So what are you waiting for? It’s time to give your mouse the ride of his life!

Getting Started

To continue with this part of the tutorial, first make sure that you have the complete project from Part Two of the tutorial, available here. You will also need to download this sound pack, which you will be using later on.

Open your project in LevelHelper and open the last level file – level03.

Implementing Collisions: Overview

The 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 to handle those collisions!

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

If you open the RocketMouseAssets SpriteHelper scene and select the coin from the “objects” sheet, you’ll see that the “Is Sensor” option is checked in the Physics tab:

That is because you want the player to trigger a collision response when he touches a coin, so that you can assign points to the user, but you don’t want the mouse to behave like it’s colliding with the coin.

What about the laser? If you look at the laser property (in the Animations.pshs scene), that sprite does not have the “Is Sensor” option selected:

Why? Don’t we want the same behavior? You do, but your need to keep track of the lasers’ animation requires that you handle them differently.

If a sprite has “Is Sensor” selected, the collision will be triggered only when the player (the mouse) first touches the sprite. But for your lasers, you 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 you have “Is Sensor” enabled for the lasers, and the player makes contact with a laser when the laser is off, you get a collision detection – but you wouldn’t fry the mouse, because after all, the laser is off.

But say that the laser then turns on. However, because the collision was already detected, you have no way of knowing if the player is still touching the laser or not.

How can you solve this? It’s easy. Using LevelHelper’s collisions behavior together with Box2D, you can disable the collision response so that the mouse will move across the laser without behaving like it’s colliding with the laser. This way, you can have collision triggers for every frame, and you’ll know if you need to kill the player.

Implementing Collisions: Coins

OK, finally time to code! Open your Xcode project, navigate to HelloWorldLayer.mm and add this new method to it:

-(void)setupCollisionHandling {
    [loader useLevelHelperCollisionHandling];
    [loader registerBeginOrEndCollisionCallbackBetweenTagA:PLAYER
                                                   andTagB:COIN
                                                idListener:self
                                               selListener:@selector(mouseCoinCollision:)];
}

Next, call this new method at the end of init (right before the call to scheduleUpdate):

[self setupCollisionHandling];

So what does the code do here?

setupCollisionHandling first tells the LevelHelperLoader instance that it wants to use LevelHelper collision handling and not create its own collision handler. My advice is: always use this option – its fast, easy and painless.

Next, register the method that LevelHelper should call whenever a collision between a sprite with the tag PLAYER (in this case the mouse sprite) and a sprite with the tag COIN (which is any coin sprite here) happens.

Remember how we set up collision tags for these in part two? LevelHelper automatically generates constants for these tags so that you can use them in code! You can control-click on one of the tags and choose “Go to definition” if you’re curious about where they are defined.

LevelHelper collision handling has multiple types of collision notifications but here you use beginOrEnd because your coin sprites are defined as sensors and Box2D handles collision notifications for sensor objects only at the “begin” state.

Now let’s add the method that will be used for collisions between the player/mouse and coin:

-(void)mouseCoinCollision:(LHContactInfo*)contact {        
    LHSprite* coin = [contact spriteB];
    if(nil != coin) {
        if([coin visible]) {
            [self scoreHitAtPosition:[coin position] withPoints:100];
        }
        [coin setVisible:NO];
    }
}

As you’ll notice, this method gets a pointer to an LHContactInfo object as a parameter. This is a special class that provides info about the collision. Go to its definition to see what other information you receive via this object.

So how do you get the coin sprite from this collision? Well, you registered the coin sprite as being tagB in a call to registerBeginOrEndCollisionCallbackBetweenTagA:andTagB:. So if tag B is the coin, then we access the sprite using [contact spriteB].

If you want to learn more about the LHContactInfo class, check out the Documentation tab in LevelHelper.

Next, you make sure that 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, you call a method (that you’ll add next) to give points to the user for grabbing the coin. You then set the coin to be invisible so as to provide visual feedback to the user that s/he now has the coin in their virtual wallet.

Next, add scoreHitAtPosition:withPoints: as follows:

-(void)scoreHitAtPosition:(CGPoint)position withPoints:(int)points {    
    score += points;
}

This means that you need to add a new score variable in HelloWorldLayer.h:

int score;

Compile and run. You’ll see that when the mouse collides with the coins, the coins disappear!

Collecting coins in our game

Implementing Collisions: Lasers

The next step is to handle the collision between the lasers and the mouse.

Add the following at the end of setupCollisionHandling in HelloWorldLayer.mm:

    [loader registerPreCollisionCallbackBetweenTagA:PLAYER
                                            andTagB:LASER
                                         idListener:self
                                        selListener:@selector(mouseLaserCollision:)];

The above code registers a pre-collision callback for collisions between the player and the lasers. Since the laser sprites are not sensors, you want to receive notifications about this type of collision on every frame in order to kill the mouse when the laser does become active.

Add the mouseLaserCollision: method:

-(void)mouseLaserCollision:(LHContactInfo*)contact {        
    LHSprite* laser = [contact spriteB];
    int frame  = [laser currentFrame];
 
    // 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 disable the contact so that the player and laser don't collide, but trigger a collision.
    // Disabling the contact is only active for one frame,
    // so on the next frame the contact will be active again, triggering the collision.
 
    b2Contact* box2dContact = [contact contact];    
    box2dContact->SetEnabled(false);
 
    if(playerIsDead)
        return;
    if(frame != 0) {
        [self killPlayer];    
    }
}

The above code gets sprite B from the contact info. In this case sprite B is the laser. Then the code takes the current frame from the sprite, because it needs to test if the laser is active so that the player can be killed if it is.

Then the code takes the Box2D contact information from the LevelHelper contact object and disables the contact so that no collision behavior will occur during the current frame. It then checks whether the player is dead. If so, nothing further is done.

Finally, the code tests if the frame number is not 0. If it is 0, then the laser is not on and the player is safe. If it’s not 0, it means that the mouse struck a live laser and so, the player has to be killed.

The above code has two elements that have not been defined yet: the killPlayer method and the playerIsDead variable. So let’s define them.

Switch to HelloWorldLayer.h and add the following instance variable:

bool playerIsDead;

Then add the killPlayer implementation to HelloWorldLayer.mm:

-(void)killPlayer {
    playerVelocity = 0.0;
    playerShouldFly = false;
    playerIsDead = true;
    playerWasFlying = false;
    [rocketFlame setVisible:NO];
    [player prepareAnimationNamed:@"mouseDie" fromSHScene:@"Animations"];
    [player playAnimation];
 
    [paralaxNode setSpeed:0];
 
    CGSize winSize = [[CCDirector sharedDirector] winSize];
    CCLabelTTF *label = [CCLabelTTF labelWithString:@"Game Over"
                                           fontName:@"Marker Felt"
                                           fontSize:64];
    label.color = ccRED;
    label.position = ccp(winSize.width*0.5, winSize.height*0.75);
    [self addChild:label];
 
    CCMenuItem *item = [CCMenuItemFont itemWithString:@"Restart"
                                               target:self
                                             selector:@selector(restartGame)];
    CCMenu *menu = [CCMenu menuWithItems:item, nil];
    [menu alignItemsVertically];
 
    [self addChild:menu];
}

This is what the above code does:

  • Set the velocity of the player to 0. If the player was flying, he will now fall to the ground.
  • Set the playerIsDead variable to TRUE so we know the player is dead in the methods where this information is a factor.
  • Hide the rocket flame.
  • 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 the animation should be performed.
  • Stop the parallax from moving by setting its speed to 0.

Note: For more details on using animations with LevelHelper, check out the LevelHelper API via the Documentation tab in LevelHelper.

Now you need to create a Cocos2d menu so that you can restart the game if the player dies. Define the necessary method method as follows:

-(void)restartGame {
    [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]];
}

Compile and run. Your mouse can now die if he collides with a laser!

Note: If a collision does not work as it should, check to make sure that you have the correct tags set on the sprites.

Mouse dies upon colliding with a laser

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 Xcode Resources 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 Resources folder, create a new folder called Music, and drag all the sound files from the sound pack into this new folder.

Now go back to Xcode and add the music assets to the project by right-clicking (or Control-clicking) on the Resources folder and choosing “Add Files to RocketMouse.”

From the new dialog, navigate to your Resources folder, select the Music folder, and click the Add button.

Your new Resources folder in Xcode should look something like this:

Now that you’ve added the sound files to your project, let’s add some code to make some noise!

At the top of HelloWorldLayer.mm import the audio engine:

#import "SimpleAudioEngine.h"

Now add a new method that will load the sounds:

-(void) setupAudio {
    [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"backgroundMusic.m4a"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"coin.wav"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"fly.wav"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"ground.wav"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"hitObject.wav"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"laser.wav"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"lose.wav"];
    [[SimpleAudioEngine sharedEngine] preloadEffect:@"bunnyHit.wav"];
}

Next, add the following line to init (right before the call to scheduleUpdate):

[self setupAudio];

This loads all the sounds in the pack – backgroundMusic.m4a is loaded as a music asset and the rest of the files are preloaded as effects. Now we have to match the sound effects to the events that will generate them.

Add the following to the end of mouseCoinCollision::

[[SimpleAudioEngine sharedEngine] playEffect:@"coin.wav"];

In mouseLaserCollision: add the following inside the if(frame != 0) block:

[[SimpleAudioEngine sharedEngine] playEffect:@"laser.wav"];

Let’s also add a flying sound. Inside update:, add the following inside the if (playerShouldFly) block:

[[SimpleAudioEngine sharedEngine] playEffect:@"fly.wav"];

Compile and run. Enjoy the new music and sound effects! :]

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

Collisions Between Mouse, Cats, and Dogs

At this point the mouse can die if he hits a laser, but he’s getting off too easy when it comes to the cats and dogs! So let’s add some collision handling for the cats and dogs.

Add the following to the end of setupCollsionHandling:

    [loader registerPreCollisionCallbackBetweenTagA:PLAYER
                                            andTagB:DOG
                                         idListener:self
                                        selListener:@selector(mouseDogCatCollision:)];
    [loader registerPreCollisionCallbackBetweenTagA:PLAYER
                                            andTagB:CAT
                                         idListener:self
                                        selListener:@selector(mouseDogCatCollision:)];

Your game will perform the same action for collisions with both cats and dogs. So you need only one method – implement it as follows:

-(void)mouseDogCatCollision:(LHContactInfo*)contact {
	[[SimpleAudioEngine sharedEngine] playEffect:@"hitObject.wav"];
    [loader cancelPreCollisionCallbackBetweenTagA:PLAYER andTagB:DOG];
    [loader cancelPreCollisionCallbackBetweenTagA:PLAYER andTagB:CAT];
    [self killPlayer];
}

The above plays the sound effect and cancels the collision callback between the player and the cat or dog, since we no longer need it. Then the player is killed.

Compile and run. Now your mouse can die if he hits a cat or dog too – so watch out! :]

Mouse dies from hitting a dog

Tweaking Gameplay

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

The first of these problems is that if you touch the screen after the mouse dies, he starts flying around like he’s been resurrected! Although “Zombie Mouse” might make for a popular game on the App Store, this is probably not the effect you’re going for ;]

Let’s put a stop to this zombie behavior before it gets out of hand! Just add the following check to the beginning of ccTouchesBegan::

if(playerIsDead)
    return;

Basically, you test to see if the player is dead. If he is, then you do nothing further.

If you run the game now, the zombie behavior won’t occur anymore. But there are other issues.

For example, in my level there are places where the mouse 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 LevelHelper, switch to level03, and drag the cats, dogs, and lasers around (as necessary) so that the player has a way through. (But don’t make it too easy!)

Make sure to save your changes to the level!

Since you’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 you did in Part Two) and add a tag named “GROUND.”

Next, add the new tag to the bottom part of the Physic Boundary shape. To do this, click the Physic Boundaries button and select the tag for the correct boundary as per the image below:

Save your level in LevelHelper.

Since you added a new tag (GROUND), make sure you also regenerate the supporting code by going to File/Generate Code.

Next, you have to define the collision callback between the player and the ground. Add the following to the end of setupCollisionHandling:

    [loader registerPreCollisionCallbackBetweenTagA:PLAYER
                                            andTagB:GROUND
                                         idListener:self
                                        selListener:@selector(mouseGroundCollision:)];

Now add the mouseGroundCollision: implementation:

-(void)mouseGroundCollision:(LHContactInfo*)contact {
    if(playerIsDead)
        return;
    if(playerWasFlying) {
        [[SimpleAudioEngine sharedEngine] playEffect:@"ground.wav"];
        [player prepareAnimationNamed:@"mouseFall" fromSHScene:@"Animations"];
        [player playAnimation];
        [player setAnimationHasEndedObserver:self selector:@selector(fallAnimHasEnded:)];
    }
    playerWasFlying = false;
}

You first test if the player is dead since you don’t want to do anything further if the mouse is dead. You then test if the player was flying when they touched the ground. If they were, you play the landing sound.

Then you start the mouseFall animation. You also register for a notification. That way, when the animation ends, you get notified so that you can set another animation on the player sprite.

Now add the method that gets called when the mouseFall animation ends as follows:

-(void)fallAnimHasEnded:(NSNotification*)notif {
    LHSprite* sprite = [notif object]; //get the sprite on which the animation has ended
 
    if(sprite == player){
        [player prepareAnimationNamed:@"mouseRun" fromSHScene:@"Animations"];
        [player playAnimation];
        [player removeAnimationHasEndedObserver];
    }
}

This new method receives a notification object sent by the animation as a parameter. You access the sprite which triggered this notification using the “object” method. Then you can find all the info about the animation from the LHSprite object.

If the sprite is the player, you prepare and play the mouseRun animation and you remove the notification observer (this is not mandatory but its best to do it).

Compile and play.

It looks like you have almost everything in place, doesn’t it? But there is still one thing you need to fix!

You may have noticed that if you run the game now, coins taken from the level don’t reappear when the parallax restarts. If you recall, you hide them after player collisions. So you need a way to make them visible again when the parallax starts over.

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

-(void) retrieveRequiredObjects
{
    // existing lines
    paralaxNode = [lh paralaxNodeWithUniqueName:@"Parallax_1"];
    NSAssert(paralaxNode!=nil, @"Couldn't find the parallax!");
 
    // add this new line
    [paralaxNode registerSpriteHasMovedToEndListener:self 
        selector:@selector(spriteInParallaxHasReset:)]; 
 
    // rest of code...

The new code sets up a call to a new method (spriteInParallaxHasReset:) that will be executed whenever a sprite in the parallax is reset to the back of the parallax. This method gets called when the sprite exits the view.

Add the new method as follows:

-(void) spriteInParallaxHasReset:(LHSprite*)sprite {    
    if(COIN == [sprite tag]){
        [sprite setVisible:YES];
    }
}

The method’s pretty simple – test the tag of the sprite that was reset by the parallax, and if it’s tagged as a COIN, make it visible again.

Compile and run. 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 (that happens in Part Four), but very close, with animations, collisions, sounds, and correct gameplay.

In case you need it, here is a 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.

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

User Comments

3 Comments

  • I am really enjoying the tutorials so far, but have had two problems. The first was the restart button not appearing. I fixed this by defining a ccp location but now when I press on it, it brings up an error on Xcode saying:

    Code: Select all
    inline b2Body* b2Joint::GetBodyA()
    {
        Return m_body
    }


    This is highlighted in green with the error message : thread 1:EXC_BAD_ACCESS(code=1,address=0x3469030)

    It might also be worth adding that it doesnt work with the downloadable file at the end of the project. This makes me think that it must be a problem with Xcode 5 and iOS7. I have tested it on all simulators, and my Iphone 5s and Ipad 2. I hope you could give me some idea, thanks!

    Jack
    Jdjack
  • After alot of trial and error, it now works
    Jdjack
  • Great tut other than the restart button not appearing, I believe that is the only thing that needs updating :)
    Water Melon

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!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Non-Gaming!

    Loading ... Loading ...

Last week's winner: How to Make a Simple 2D Game with Metal.

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in October: Xcode 6 Tips and Tricks!

Sign Up - October

Our Books

Our Team

Tutorial Team

  • Ron Kliffer
  • Jake Gundersen

... 53 total!

Update Team

  • Zouhair Mahieddine

... 14 total!

Editorial Team

... 22 total!

Code Team

  • Orta Therox

... 3 total!

Subject Matter Experts

... 4 total!