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 3 of 4 of this article. Click here to view the first page.

The HUD Layer – the Score

Now let's add a score display to the HUD. For the score, I suggest using the highest point the monkey has reached while standing on an object.

Switch to Monkey.h and add a new variable and property:

    float score;
    @property (nonatomic, readonly) float score;

Switch to Monkey.mm and synthesize the score property at the beginning of the file:

    @synthesize score;

Add the following lines to the end of updateCCFromPhysics:

    // 9 - update score
    if(numFloorContacts > 0)
    {
        float s = [self physicsPosition].y * 10;
        if(s> score)
        {
            score = s;
        }
    }

Note that you update the score only if it is higher than the current score because sometimes the monkey might drop down to a lower position after climbing higher. you also scale the monkey's y-value by 10. Otherwise the score increases are fairly low and not very motivating.

Switch to Hud.h. Add a define for the number of score digits:

    #define MAX_DIGITS 5

Add variables to keep the digit sprites and to cache the CCSpriteFrame pointers:

    CCSprite *digits[MAX_DIGITS];  // weak references
    CCSpriteFrame *digitFrame[10]; // weak references

Add a method definition to set the score:

    -(void) setScore:(float) score;

Now switch to Hud.mm. The first thing to do here is cache the lookup of the digit sprites. Add the following lines to the end of the init method:

    // 2 - Cache sprite frames
    CCSpriteFrameCache *sfc = [CCSpriteFrameCache sharedSpriteFrameCache];
    for(int i=0; i<10; i++)
    {
        digitFrame[i] = [sfc spriteFrameByName:
                         [NSString stringWithFormat:@"numbers/%d.png", i]];
    }

    // 3 - Init digit sprites
    for(int i=0; i<MAX_DIGITS; i++)
    {
        digits[i] = [CCSprite spriteWithSpriteFrame:digitFrame[0]];
        digits[i].position = ccp(345+i*25, 290);
        [self addChild:digits[i]];
    }

Here, you use the CCSpriteFrameCache and request the frame for each digit. You'll store the frame data in the digitFrame array. Then you create sprites for each digit to display and initialize each one to frame 0.

Add the following method to the end of the file - it prints the current score in a character buffer and adjusts the digits displayed according to the digits in the buffer:

-(void) setScore:(float) score
{
    char strbuf[MAX_DIGITS+1];
    memset(strbuf, 0, MAX_DIGITS+1);
    
    snprintf(strbuf, MAX_DIGITS+1, "%*d", MAX_DIGITS, (int)roundf(score));
    int i=0;
    for(; i<MAX_DIGITS; i++)
    {
        if(strbuf[i] != ' ')
        {
            [digits[i] setDisplayFrame:digitFrame[strbuf[i]-'0']];
            [digits[i] setVisible:YES];
        }
        else
        {
            [digits[i] setVisible:NO];
        }
    }
}

Finally, switch to GameLayer.mm and add this code to the end of the update method:

    // 12 - Show the score
    [hud setScore:monkey.score];

Build and run. Check if the score is updated when the monkey climbs higher. The monkey starts with a score of 9 – this is because the floor's height already adds to the monkey's score. If you want you can reduce 9 from the score so it starts at 0.

All of the code up to this point is available in the folder 6-Hud.

Getting Hungry

Currently, the monkey gets hurt by the falling bananas, but I want them to restore his health when he consumes them.

To enable this, you'll create a subclass of Object called ConsumableObject. This class gets a bool variable that keeps track as to whether the object has already been consumed.

I usually prefer using one file for each class, but since these classes are quite small, I'm going to add it to the end of Object.h (after @end):

    @interface ConsumableObject : Object 
    {
    @protected
        bool consumed;
    }
    -(void)consume;
    @end

Similarly, derive Banana and BananaBunch classes by adding the following code after the definition of ConsumableObject:

    @interface Banana : ConsumableObject
    {
    }
    @end

    @interface BananaBunch : ConsumableObject
    {
    }
    @end

Now implement the consume method for ConsumableObject in Object.mm. It's important to add the code below the @end that closes the @implementation for Object:

    @implementation ConsumableObject

    -(void) consume
    {
        if(!consumed)
        {
            // set consumed
            consumed = YES;

            // fade & shrink object
            // and delete after animation
            [self runAction:
             [CCSequence actions:
              [CCSpawn actions:
               [CCFadeOut actionWithDuration:0.1],
               [CCScaleTo actionWithDuration:0.2 scale:0.0],
               nil],
              [CCCallFunc actionWithTarget:self selector:@selector(deleteNow)],
              nil]
             ];   

            // play the item consumed sound
            // pan it depending on the position of the monkey
            // add some randomness to the pitch
            [[SimpleAudioEngine sharedEngine] playEffect:@"gulp.caf" 
                    pitch:gFloatRand(0.8,1.2)
                    pan:(self.ccNode.position.x-240.0f) / 240.0f 
                    gain:1.0 ];
        }
    }

    @end

The consume method checks to see if the object has already been consumed. If it hasn't been consumed, then scale the object to 0 and fade it out, and finally, delete the object from the game.

To do this, you create a CCSequence action with a parallel action of CCFadeOut and CCScaleTo, followed by a CCCallFunction. This CCCallFunction calls the deleteNow selector. This selector removes a GB2Node object from the world, both in graphics and physics.

Now, switch to Monkey.h and add the new restoreHealth method:

    -(void)restoreHealth:(float)amount;

Next, switch to Monkey.mm and implement the method at the end of the class:

    -(void) restoreHealth:(float)amount
    {
         health = MAX(health + amount, MONKEY_MAX_HEALTH);
    }

Here, you simply add the new health value, ensuring that it does not exceed the maximum. Just setting the health is enough as the HUD takes care of animating the health bar.

You'll also play a small gulp sound when the monkey swallows the item. To do this, import Monkey.h at the beginning of Object.mm:

    #import "Monkey.h"

Then, implement the beginContactWithMonkey for the Banana and BananaBunch classes below the implementation for ConsumableObject in Object.mm:

    @implementation Banana
    -(void) beginContactWithMonkey:(GB2Contact*)contact
    {
        if(!consumed)
        {
            Monkey *monkey = (Monkey *)contact.otherObject;
            [monkey restoreHealth:20];        
            [self consume];
        }
    }
    @end

    @implementation BananaBunch
    -(void) beginContactWithMonkey:(GB2Contact*)contact
    {
        if(!consumed)
        {
            Monkey *monkey = (Monkey *)contact.otherObject;
            [monkey restoreHealth:60];        
            [self consume];
        }
    }
    @end

We simply check if the object was already consumed, and if not, call restoreHealth on the Monkey object. The banana restores 20 points, while the banana bunch restores 60 points.

Build and run. Hey – what's that? It's not working!

Objective Thinking

The reason for failure? Bananas and banana bunches are still created as Object classes. The factory method you use in Object.mm does not yet create your new Banana and BananaBunch objects.

Go back to Object.mm and change the randomObject selector to produce Banana and BananaBunch objects:

    +(Object*) randomObject
    {
        NSString *objName;
        switch(rand() % 18)
        {
            case 0:
                // create own object for bananas - for separate collision detection
                return [[[Banana alloc] initWithObject:@"banana"] autorelease];

            case 1:
                // create own object for banana packs - for separate collision detection
                return [[[BananaBunch alloc] initWithObject:@"bananabunch"] autorelease];

            case 2: case 3: case 5:
            ...

Build and run. Nice!

The only thing that bothers me is that the monkey stops when hitting a banana and the bananas bounce off the monkey.

Box2d has two phases during the stepping of its world: a presolve phase and a collision phase. During the presolve phase it is possible to disable collisions between objects. The collision callbacks will get called, but the objects won't bounce off.

GBox2D wraps this into a selector called presolveContactWith* that can be called on the colliding objects. Within this selector, you can disable the contact.

Add the following selector to ConsumableObject in Object.mm (before the @end marker) – it will fix the collisions for Banana and BananaBunch:

-(void) presolveContactWithMonkey:(GB2Contact*)contact
{
    [contact setEnabled:NO];
}

Build and run. Check if the monkey can eat the banana without getting disturbed or having the banana bounce off him.