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

In part third and final part of the Monkey Jump tutorial series, you will add the HUD layer, some performance improvements, and yes – kill the monkey! :] 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.

Putting the Pain on your Hero

Play the game for a bit and you'll realize that there isn't much challenge to this game – the monkey climbs but doesn't take any damage. Let's change that.

Open Monkey.h and add a new variable called "health" to the Monkey class.

    float health;

Also add properties to access the health level and to detect if the monkey is dead:

@property (readonly) float health;
@property (readonly) bool isDead;

Finally, add a define for the maximum health, at the top of the file below the import statements:

    #define MONKEY_MAX_HEALTH 100.0f

Now switch to Monkey.mm and synthesize the health property:

    @synthesize health;

Implement the isDead property by adding the following code above the walk method :

    -(bool) isDead
    {
        return health <= 0.0f;
    }

As you'll notice, you decide if the monkey is dead or not based on if his health is less than 0 or not.

In the init selector, initialize the health with the maximum value by adding this below the game layer storage code:

    // set health
    health = MONKEY_MAX_HEALTH;

Now let's put the hurt on the monkey by modifying the section with the head collision in beginContactWithObject:

    else if([fixtureId isEqualToString:@"head"])
    {
        numHeadContacts++;
        float vY = [contact.otherObject linearVelocity].y;

        if(vY < 0)
        {
            const float hurtFactor = 1.0;            
            // reduce health
            health += vY*[contact.otherObject mass] * hurtFactor;            
            if(self.isDead)
            {
                // set monkey to collide with floor only
                [self setCollisionMaskBits:0x0001];                
                // release rotation lock
                [self setFixedRotation:NO];                
                // change animation phase to dead
                [self setDisplayFrameNamed:@"monkey/dead.png"];
            }
        }        
    }

Basically, the monkey should get hurt when an object collides with his head. Calculate the damage using the object's vertical velocity and mass, and reduce the monkey's health by that amount. This causes damage from fast-dropping objects, but doesn't harm the monkey if an object is resting above his head. Notice that I also added a hurtFactor so that you can adjust how much the monkey is hurt.

If the monkey dies, he should drop from the scene. In this case, you'll simply delete all the monkey's collision flags except for the floor. This will make the monkey fall dead on the floor. You'll release the rotation lock to let him lie on the floor, and change the monkey's sprite to dead.png.

Dead monkeys can't jump – so change the code in the monkey's jump selector to ignore screen taps if the monkey is dead:

    -(void) jump
    {
        if((numFloorContacts > 0) &&  (!self.isDead))
        {
            ...

Disable the updateCCFromPhysics contents by changing section #1, as well:

    -(void) updateCCFromPhysics
    {
        // 1 - Call the super class
       [super updateCCFromPhysics];
        // he's dead – so just let him be!
        if(self.isDead)
        {
            return;
        }
        
        ...

Build and run, and now you can bring out your evil side - kill the monkey! :]

Restarting the Game

Now the monkey dies, but the objects keep falling and there's no way to restart the game.

I would suggest restarting two seconds after the monkey's death. Usually, we'd go to a high score table after a game ends, but that's too much for this tutorial. A simple restart will suffice.

Add a new variable to GameLayer.h to hold the restart timer:

    ccTime gameOverTimer;  // timer for restart of the level

And add these lines to the beginning of update inside GameLayer.mm:

    if(monkey.isDead)
    {
        gameOverTimer += dt;
        if(gameOverTimer > 2.0)
        {
            // delete the physics objects
            [[GB2Engine sharedInstance] deleteAllObjects];
            
            // restart the level
            [[CCDirector sharedDirector] replaceScene:[GameLayer scene]];            
            return;
        }
    }

In case of a restart, you simply remove all objects from the GB2Engine and replace the current scene with a new GameLayer.

Build and run. The level should now restart two seconds after the monkey's death.

The HUD layer – Health

Yes, the monkey dies, but no one knows when it's going to happen! That's too realistic for me and most other players. Let's add a health display so that you can keep track of the monkey's health.

You're going to represent the monkey's health with 10 banana icons. Each banana represents 10 points of health.

Create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Hud, and make it a subclass of CCSpriteBatchNode. And don't forget to change the .m extension to .mm. Replace the contents of the Hud.h file with the following:

    #pragma once

    #import "Cocos2d.h"

    #define MAX_HEALTH_TOKENS 10

    @interface Hud : CCSpriteBatchNode
    {
        CCSprite *healthTokens[MAX_HEALTH_TOKENS]; // weak references
        float currentHealth;
    }

    -(id) init;
    -(void) setHealth:(float) health;

    @end

The HUD display uses the sprites from the jungle sprite sheet, so you have to derive the HUD from CCSpriteBatchNode in order to have access to the jungle sprite sheet sprites. Additionally, the HUD needs to keep track of the current health (which you will need later) and the sprite representing each point of the monkey's health. you also need a method to change the current health.

Switch to Hud.mm and replace its contents with the following:

    #import "Hud.h"
    #import "Monkey.h"
    #import "GMath.h"

    @implementation Hud

    -(id) init
    {    
        self = [super initWithFile:@"jungle.pvr.ccz" capacity:20];

        if(self)
        {
            // 1 - Create health tokens
            for(int i=0; i<MAX_HEALTH_TOKENS; i++)
            {
                const float ypos = 290.0f;
                const float margin = 40.0f;
                const float spacing = 20.0f;
                
                healthTokens[i] = [CCSprite spriteWithSpriteFrameName:@"hud/banana.png"];
                healthTokens[i].position = ccp(margin+i*spacing, ypos);
                healthTokens[i].visible = NO;
                [self addChild:healthTokens[i]];            
            }        
        }

        return self;
    }
    
    @end

Here, you initialize the HUD's CCSpriteBatchNode super class with the sprite sheet.

Then, you iterate through the number of health tokens and create sprites for each of the bananas. you also increase the x-position of each banana to lay it out next to the previous banana.

Finally, add the method to update the health to the end of Hud.mm:

    -(void) setHealth:(float) health
    {
        // 1 - Change current health
        currentHealth = health;
    
        // 2 - Get number of bananas to display
        int numBananas = round(MAX_HEALTH_TOKENS * currentHealth / MONKEY_MAX_HEALTH);
    
        // 3 - Set visible health tokens
        int i=0;
        for(; i<numBananas; i++)
        {
            healthTokens[i].visible = YES;
        }
    
        // 4 - Set invisible health tokens
        for(; i<MAX_HEALTH_TOKENS; i++)
        {
            healthTokens[i].visible = NO;
        }
    }

In this method, you need to determine the number of bananas to display, make the ones to display visible, and clear the invisible ones. It's possible for sections #3 and #4 to be implemented with only one loop, but You're going to extend this code later and so you will have that as two separate loops.

Next you need to add the new HUD to the GameLayer. Switch to GameLayer.h and add the predeclaration of the HUD class:

    @class Hud;

Then, add a member variable for the HUD to the GameLayer class:

    Hud *hud;

Switch to GameLayer.mm and import Hud.h at the start of the file:

    #import "Hud.h"

Init the HUD inside the init selector:

    // add hud
    hud = [[[Hud alloc] init] autorelease];
    [self addChild:hud z:10000];

Finally, update the HUD from inside the update selector by adding this code to the very end:

    // 11 - Show monkey's health in bananas
    [hud setHealth:monkey.health];

Build and run. It works, but I don't quite like the visuals - I don't think the bananas should appear and disappear so abruptly. I want them to fade in and out. I also think the monkey's health should drop over time rather than instantly.

Since setHealth gets called every frame, it won't be hard to adjust the displayed health level over time.

Open Hud.mm and change the setHealth selector's section #1 with the following:

    // 1 - Change current health
    float healthChangeRate = 2.0f;    
    // slowly adjust displayed health to monkey's real health
    if(currentHealth < health-0.01f)
    {
        // increase health - but limit to maximum
        currentHealth = MIN(currentHealth+healthChangeRate, health);
    }
    else if(currentHealth > health+0.01f)
    {
        // reduce health - but don't let it drop below 0
        currentHealth = MAX(currentHealth-healthChangeRate, 0.0f);
    }
    currentHealth = clamp(currentHealth, 0.0f, MONKEY_MAX_HEALTH);

Build and run. Now the HUD adjusts more slowly, but the bananas still disappear way too quickly. Let's make them fade and scale in and out.

Replace sections #3 and #4 in setHealth with the following code:

    // 3 - Set visible health tokens
    int i=0;
    for(; i<numBananas; i++)
    {
        if(!healthTokens[i].visible)
        {
            healthTokens[i].visible = YES;
            healthTokens[i].scale = 0.6f;
            healthTokens[i].opacity = 0.0f;
            // fade in and scale
            [healthTokens[i] runAction:
             [CCSpawn actions:
              [CCFadeIn actionWithDuration:0.3f],
              [CCScaleTo actionWithDuration:0.3f scale:1.0f],
              nil]];
        }
    }

    // 4 - Set invisible health tokens
    for(; i<MAX_HEALTH_TOKENS; i++)
    {
        if(healthTokens[i].visible && (healthTokens[i].numberOfRunningActions == 0) )
        {
            // fade out, scale to 0, hide when done
            [healthTokens[i] runAction:
             [CCSequence actions:
              [CCSpawn actions:
               [CCFadeOut actionWithDuration:0.3f],
               [CCScaleTo actionWithDuration:0.3f scale:0.0f],
               nil],
              [CCHide action]
              , nil]
             ];
        }
    }

To fade a banana into view, you check if the banana is already visible. If it's not, you set it to visible, set the scale to be smaller than the actual size and opacity to 0, and then run an action scaling the banana to 1.0 and fading it in. If the banana is already visible, you'll do nothing since an action might already be running on it.

To fade a banana out of view, you need a sequence action: first scale and fade out, and then set it to invisible using the CCHide action.

Since you can't use the visible flag to determine if the banana is fading out, you'll check the number of animations running on the banana. If the number isn't zero, that means an animation is already running, so you won't run another one.

Build and run. Watch for the bananas to fade in on start and fade out when the monkey gets hurt.

Awesome!