How To Build a Monkey Jump Game Using Cocos2d 2.X, PhysicsEditor & TexturePacker – Part 2

In part 2 of this Monkey Jump tutorial series, you will add the hero to the game, make him move and jump, and start adding some gameplay. By .

Leave a rating/review
Save for later
Share

Contents

Hide contents

How To Build a Monkey Jump Game Using Cocos2d 2.X, PhysicsEditor & TexturePacker – Part 2

30 mins

Create this vertical scrolling platformer with Cocos2d!

Create this vertical scrolling platformer with Cocos2D 2.X!

Welcome back to the MonkeyJump tutorial! In this series, you are creating a fun vertical scrolling platformer with Cocos2d, TexturePacker, and PhysicsEditor.

In Part One of the tutorial series, you learned about the MonkeyJump game design, created the sprite sheets and shapes you needed, and began coding the game.

Before you stopped for a break, you had all of your game layers set up and had just finished making random objects drop from the sky, with sound effects.

In this second part of the tutorial, you will add the hero to the game, make him move and jump, and start adding some gameplay.

You’ll be starting with the project where you left off last time. If you don’t have it already, grab the source code for this tutorial series and open up 3-DroppingObjects.

Without further ado, let’s get back to (monkey) business! :]

Getting Started

You created the monkey’s shape in PhysicsEditor in Part One of this tutorial, but haven’t added the monkey to the game yet. Let’s do that now!

Add the Monkey class by creating a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Monkey, and make it a subclass of GB2Sprite. Again, remember to change extension for the Monkey.m file to .mm.

The monkey will react to different events in the game world. For example, he’ll put up his hands when something drops from above, push items, and jump. This is why you won’t be using Cocos2d’s standard animation routines, and instead implement some of your own.

For this, you need some member variables to store the additional data. Paste this code into Monkey.h replacing what’s there already:

    #pragma once

    #import "Cocos2d.h"
    #import "GB2Sprite.h"

    @class GameLayer;

    @interface Monkey : GB2Sprite
    {
        float direction;      // keeps monkey's direction (from accelerometer)
        int animPhase;        // the current animation phase
        ccTime animDelay;     // delay until the next animation phase is stated
        GameLayer *gameLayer; // weak reference
    }

    -(id) initWithGameLayer:(GameLayer*)gl;
    -(void) walk:(float)direction;

    @end

Now switch to Monkey.mm and replace it with the following lines:

    #import "Monkey.h"
    #import "GB2Contact.h"
    #import "GMath.h"
    #import "Object.h"
    #import "SimpleAudioEngine.h"
    #import "GameLayer.h"

    #define JUMP_IMPULSE 6.0f
    #define WALK_FACTOR 3.0f
    #define MAX_WALK_IMPULSE 0.2f
    #define ANIM_SPEED 0.3f
    #define MAX_VX 2.0f

    @implementation Monkey
    
    -(id) initWithGameLayer:(GameLayer*)gl
    {   
        // 1 - Initialize the monkey 
        self = [super initWithDynamicBody:@"monkey"
                    spriteFrameName:@"monkey/idle/1.png"];

        if(self)
        {
            // 2 - Do not let the monkey rotate
            [self setFixedRotation:true];

            // 3 - The monkey uses continuous collision detection
            // to avoid getting stuck inside fast-falling objects
            [self setBullet:YES];

           // 4 - Store the game layer
            gameLayer = gl;
        }

        return self;
    }

    @end    

Let’s go through the initWithGameLayer method step-by-step:

  1. First, you initialize the monkey. The monkey’s movement will be affected by the physics engine, so make him a dynamic object. You’ll use the idle frame as both the first frame of the monkey animation and the monkey’s physics shape.
  2. The monkey should stand straight up all the time, so you set his rotation to fixed. This means the monkey is moved by Box2d, but does not rotate or tilt.
  3. Set the monkey to bullet mode. Bullet mode enables continuous collision detection on an object. Without it, Box2d moves objects and then performs the collision checks. With fast-moving objects it’s possible that an object will pass through another without any collision detection at all or that an object will get stuck in another. Continuous collision detection calculates collisions all the way from an object’s current position to its new position – not just for the end point.
  4. Finally, you need to store the game layer – keep it as a weak reference and just assign the value.

A monkey bullet! Bullet mode enables continuous collision detection.

A monkey bullet! Bullet mode enables continuous collision detection.

With regards to bullet mode for step #3, if you were coding a project with only a few objects, you could set the Box2d engine to run continuous collision detection for all game objects. However, when a game has a lot of objects, that would add a lot of CPU overhead. So, for your game you’ll set the continuous mode on just the monkey and fast-moving (dropping) objects.

To enable the monkey, add him to the GameLayer. Open GameLayer.h and add the following lines at the top, just below the import statements:

    @class Monkey;

Now, add the following member variable to the GameLayer class:

    Monkey *monkey;

Then switch to GameLayer.mm and import Monkey.h at the top of the file:

    #import "Monkey.h"

At the end of the init selector in GameLayer.mm, initialize the monkey, add him to the game layer and set a starting position for the monkey:

    monkey = [[[Monkey alloc] initWithGameLayer:self] autorelease];
    [objectLayer addChild:[monkey ccNode] z:10000];
    [monkey setPhysicsPosition:b2Vec2FromCC(240,150)];

Compile and run, and you’ll see the following:

The monkey is in the house! Objects drop onto him and he gets pushed away – perfect. That’s exactly what you want.

Do the Monkey Walk

Your next goal is to make the monkey walk, using the accelerometer as input.

Go back to GameLayer.mm and add the following code at the end of the init method:

    self.isAccelerometerEnabled = YES;

This will ensure that for each change in the built-in accelerometer values, the GameLayer class gets an automatic notification. The notification handler has to be added to GameLayer.mm at the end of the file, before the @end marker:

    - (void)accelerometer:(UIAccelerometer*)accelerometer 
            didAccelerate:(UIAcceleration*)acceleration
    {
        // forward accelerometer value to monkey
        [monkey walk:acceleration.y];
    }

The accelerometer handler calls the walk method of the monkey object with the y-axis value of the accelerometer. This method will handle actually moving the monkey back and forth based on the accelerometer input.

So, move to Monkey.mm and add the walk method to the end of the file (before the @end marker). This method simply stores the new movement direction for the monkey in a member variable.

    -(void) walk:(float)newDirection
    {
        direction = newDirection;
    }

Try compiling and running the code now … Surprise! Nothing new happens. This is due to the fact that while the direction value has been stored, it has not been applied to the physics simulation yet. In order to update the physics simulation based on the new movement direction, you need to override the updateCCFromPhysics selector, which is called by the GB2Engine on every frame for a GB2Node object to update the physics.

Update Monkey Physics

Add the following code to Monkey.mm:

    -(void) updateCCFromPhysics
    {
        // 1- Call the super class
        [super updateCCFromPhysics];

        // 2 - Apply the directional impulse
        float impulse = clamp(-[self mass]*direction*WALK_FACTOR, 
                              -MAX_WALK_IMPULSE, 
                              MAX_WALK_IMPULSE);            
        [self applyLinearImpulse:-b2Vec2(impulse,0) point:[self worldCenter]];        
    }

In the above, you first call the selector of the super class. This will update the monkey’s sprite based on physics simulation.

Then, you push the monkey in the right direction based on the stored direction value. It is important not to take complete control of the monkey. Otherwise, his “natural” behavior in response to events such as items dropping or collision detection won’t work properly.

All you really do is to give the monkey a nudge in the right direction. Since the physics engine updates happen 60 times per second, it’s important to keep the push quite light. It’s a monkey – not a bullet, even if he is in bullet mode!

You can move a Box2D object by applying an impulse to the object. And you do this using the GB2Sprite’s applyLinearImpulse method, which takes two parameters: the impulse to apply and the point of application.

For the point of application, You’ll use the world center of the object. Applied at the world center, the object will be pushed without any torque that would result into a rotation. (Which, by the way, would not happen to the monkey anyway, since you already set him not to rotate.)

When applying an impulse, I recommend using the mass of the object, which you can get with [self mass]. This is because the impulse is the product of mass and velocity.

Multiply the mass value by the stored direction value. This gives you small impulses when the device is tilted slightly, and bigger impulses when it’s tilted sharply.

Scaling the impulse with the object’s mass frees us from having to worry about the movement changing when you change the object’s shape in PhysicsEditor. If you didn’t scale the impulse and later you made the monkey a bit smaller (for example), the same impulse applied to a monkey of less mass will result in a faster-moving monkey.

You will also clamp the value to a maximum to avoid impulses that are too strong. The maximum impulse is defined with the MAX_WALK_IMPULSE variable.

Compile and run. Still nothing? Ah, I forgot to tell you one thing: the iPhone simulator does not simulate the accelerometer. So, from now on, you need to test on the device! Switch to the device and test.

The monkey now slides left and right – but the movement doesn’t look very natural.

Make the Monkey Move

We’re going to add some code to Monkey.mm to get the monkey animations working. Add the following to the end of the updateCCFromPhysics method:

    animDelay -= 1.0f/60.0f;
    if(animDelay <= 0)
    {
        animDelay = ANIM_SPEED;
        animPhase++;
        if(animPhase > 2)
        {
            animPhase = 1;
        }
    }

The first line simply updates the time till the next animation by decreasing the time delay till the next animation phase. I use the value 1.0f/60.0f because I assume that the application runs at 60 fps and the updateCCFromPhysics method does not have a delta time parameter which would provide the timer interval between each update accurately.

If the animation time delay drops below zero, reset the animation delay value to the animation speed and increase the current phase by one. If the highest phase is reached, loop back to 1 so that the animation will continue to play in a loop.

Next, you need to determine the direction the monkey is facing. There are two ways to do this:

  1. Use the direction from the accelerometer
  2. Use the monkey’s velocity vector

I prefer to use the accelerometer, since it gives the player immediate feedback when he or she tries to change the direction by tilting the device. You’ll respond to velocity changes via the accelerometer later.

Add this code to the end of updateCCFromPhysics:

    // determine direction of the monkey
    bool isLeft = (direction < 0);

    // direction as string
    NSString *dir = isLeft ? @"left" : @"right";    

    // update animation phase
    NSString *frameName;
    const float standingLimit = 0.1;
    float vX = [self linearVelocity].x;
    if((vX > -standingLimit) && (vX < standingLimit))
    {
        // standing
        frameName = [NSString stringWithFormat:@"monkey/idle/2.png"];            
    }
    else
    {
        // walking
        NSString *action = @"walk";
        frameName = [NSString stringWithFormat:@"monkey/%@/%@_%d.png", action, dir, animPhase];        
    }

    // set the display frame
    [self setDisplayFrameNamed:frameName];

Basically, all the code above does is, if the monkey's speed is lower than standingLimit, it makes him look directly at the player with his idle animation frame. Otherwise, it uses a walk display frame matching the current direction and animation frame number.

Compile and run. The monkey now runs around – nice!

Slow Down, Little Fella

I'm still not happy with one thing: I think the monkey moves too fast. you could reduce the impulse we're applying to make him walk, but this will also make him slow and clumsy.

You need a strong enough impulse to make him react fast – but not too fast.

Replace the current code for updateCCFromPhysics in Monkey.mm with the following code:

    // 1 - Call the super class
    [super updateCCFromPhysics];
        
    // 2 - Update animation phase
    animDelay -= 1.0f/60.0f;
    if(animDelay <= 0)
    {
        animDelay = ANIM_SPEED;
        animPhase++;
        if(animPhase > 2)
        {
            animPhase = 1;
        }
    }
    
    // 3 - Get the current velocity
    b2Vec2 velocity = [self linearVelocity];
    float vX = velocity.x;
    
    // 4 - Determine direction of the monkey
    bool isLeft = (direction < 0);
    
    if((isLeft && (vX > -MAX_VX)) || ((!isLeft && (vX < MAX_VX))))
    {
        // apply the directional impulse
        float impulse = clamp(-[self mass]*direction*WALK_FACTOR, 
                              -MAX_WALK_IMPULSE, 
                              MAX_WALK_IMPULSE);            
        [self applyLinearImpulse:-b2Vec2(impulse,0) point:[self worldCenter]];        
    }

    // 5 - Get direction as string
    NSString *dir = isLeft ? @"left" : @"right";        

    // 6 - Update animation phase
    NSString *frameName;
    const float standingLimit = 0.1;
    if((vX > -standingLimit) && (vX < standingLimit))
    {
        // standing
        frameName = [NSString stringWithFormat:@"monkey/idle/2.png"];            
    }
    else
    {
        // walking
        NSString *action = @"walk";
        frameName = [NSString stringWithFormat:@"monkey/%@/%@_%d.png", action, dir, animPhase];        
    }
    
    // 7 - Set the display frame
    [self setDisplayFrameNamed:frameName];

As you'll notice, we've moved a few code blocks around, but the major change is the addition of a new line to section #3 for the vX variable and moving the impulse code to section #4 to be wrapped within an if condition that checks if the velocity is below a maximum value for the current direction. This allows the monkey to steer against being pushed away by an object, but keeps him from accelerating too fast on his own.

Compile and run. I think this looks much better now.

The source code of the project in its current state is available in the folder 4-WalkingMonkey.