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

Andreas Löw
Create this vertical-scrolling platformer with Cocos2D!

Create this vertical-scrolling platformer with Cocos2d!

Welcome to the final part of the Monkey Jump tutorial! In this series, you are creating a fun vertical-scrolling platformer with Cocos2d 2.X, TexturePacker, and PhysicsEditor.

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

In Part Two, you added your hero to the game, made him move and jump, and added some gameplay.

In this third and final part of the series, you will add some performance improvements, add a HUD layer, and yes – kill the monkey! :]

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 5-MonkeyJumpAndRun.

All right, time to stop monkeying around and wrap up this tutorial! :]

Too Many Objects!

If you play the game for a while, you’ll see that it gets slower and slower, until it becomes completely unplayable.

There’s a reason for this – as objects continue to fall from the sky, one after another, they bump into the objects already lying around. All of these collisions have to be handled by Box2d. If there are n objects, there are n*(n-1) possible collisions to handle. Box2d uses hashes to make things faster, but you can imagine how dramatically the number of collisions increases as the number of objects increases.

If you watch a statue drop, you’ll see that nearly the entire stack below moves and bounces from impulses passed from the statue down through the stack from one object to another.

To improve the situation, you’re going to convert objects which are some distance below the monkey into static objects. These static objects will still let other objects pile up above them, but they’ll no longer react to the impact. As a result, a falling statue will only affect the top of the stack instead of the complete pile.

Box2d allows objects to go to sleep when they aren’t touched by other objects for some time. you will use this feature to improve performance.

GB2Engine has an iterate method that can be used to iterate all objects with a block. you will use it to create a routine that checks the y-coordinates of all objects and puts to sleep any that are a certain distance below the monkey.

Add the following code to the end of the update selector in GameLayer.mm:

    // 10 - Iterate over objects and turn objects into static objects
    // if they are some distance below the monkey
    float pruneDistance = 240/PTM_RATIO;
    float prune = [monkey physicsPosition].y - pruneDistance;    
    [[GB2Engine sharedInstance] iterateObjectsWithBlock: ^(GB2Node* n) 
     {
         if([n isKindOfClass:[Object class]])
         {
             Object *o = (Object*)n;
             float y = [o physicsPosition].y;
             if(y < prune)
             {
                 // set object to static
                 // if it is below the monkey
                 [o setBodyType:b2_staticBody];
             }
         }
     }
     ];

Build and run. Note that there are still some situations where things won’t work as expected. For example, if a group of objects pile up like a tower, and the monkey climbs onto the pile, it might happen that a dropping object reaches the prune distance and is converted into a static object in mid-air.

The solution is quite simple: only convert objects to static if their speed is low. Change the second if condition in the above code to:

    if((y < prune) && 
      ([o linearVelocity].LengthSquared() < 0.1))    
    {...}

Build and run. Looks good, doesn’t it?

Caught in a Trap

There’s still an issue, though – the monkey might still get caught under a pile of objects. Items pile up around him, and he’s not strong enough to break free if there are too many objects above him.

There are several ways to deal with this situation. One is to simply let him die if he’s stuck. Another solution is to “teleport” the monkey above the objects and let him go on playing. That sounds fun, let’s do that!

To make this work, you have to be sure that the monkey teleports above all the objects. Otherwise, his position might place him directly inside another object and he’ll never break free!

Go into GameLayer.h and add a member variable:

    float highestObjectY;   // y position of the highest object

And make it a property by adding the following line above the “scene” method declaration:

    @property (nonatomic, readonly) float highestObjectY;

Now switch to GameLayer.mm and synthesize the object by adding this line just below the @implementation line:

    @synthesize highestObjectY;

Then, replace section #10 in update with the following:

    // 10 - Iterate over objects and turn objects into static objects
    // if they are some distance below the monkey
    float pruneDistance = 240/PTM_RATIO;
    float prune = [monkey physicsPosition].y - pruneDistance;
    highestObjectY = 0.0f;
    [[GB2Engine sharedInstance] iterateObjectsWithBlock: ^(GB2Node* n) 
     {
         if([n isKindOfClass:[Object class]])
         {
             Object *o = (Object*)n;
             float y = [o physicsPosition].y;
             // record the highest object
             if((y > highestObjectY) && ([o active]))
             {
                 highestObjectY = y;
             }   
             if((y < prune) && 
                ([o linearVelocity].LengthSquared() < 0.1))
             {
                 // set object to static
                 // if it is below the monkey
                 [o setBodyType:b2_staticBody];
             }
         }
     }
     ];

The new code determines the highest object location by resetting the highestObjectY with every new check. Note that it only checks active objects. Otherwise, the highest object will always be the object that is waiting to drop.

Switch to Monkey.h and add a new member:

    int stuckWatchDogFrames; // counter to detect if monkey is stuck

Now let’s teleport the monkey above the highest object’s position if he’s been stuck with an object above his head for a certain amount of time. Add the following to the end of the updateCCFromPhysics selector:

    // 8 - Check if monkey is stuck
    if(numHeadContacts > 0)
    {
        stuckWatchDogFrames--;
        if(stuckWatchDogFrames == 0)
        {
            // teleport the monkey above the highest object
            [self setPhysicsPosition:b2Vec2([self physicsPosition].x, 
                                            gameLayer.highestObjectY+2.0)];                
        }
    }
    else
    {
        // restart watchdog
        stuckWatchDogFrames = 120; // 2 seconds at 60fps
    }

Build and run. That’s much better! Now, if the monkey is caught in a trap and he can’t push his way out, he will be magically freed.

There are other ways to detect if the monkey is caught. For example, you could check how high the objects are piled, or if the monkey’s speed is low for a certain amount of time. Feel free to try out other detection methods to see which one works best for you.

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!

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.

Final improvements

Your game is looking awesome! But you can still make a few improvements.

The game is a bit unfair right now: the monkey is on the scene and BAM! – a statue kills him instantly. It is a game that can be won more by chance than by skill.

To make the gameplay a bit more even, You’re going to add a drop indicator. It will be a small red bar that shows the position of the next object drop.

Go to GameLayer.h and add the following variable for the drop indicator:

    CCLayerColor *objectHint; // weak reference

Then, add the following initialization code to the very end of the init method of GameLayer.mm:

    // object Hint
    objectHint = [CCLayerColor layerWithColor:ccc4(255,0,0,128) 
                                        width:10.0f 
                                       height:10.0f];
    [self addChild:objectHint z:15000];
    objectHint.visible=NO;

You create a semi-transparent red box as the drop indicator and set the box dimensions to 10×10 pixels. You’ll resize it later to match the dropping object’s size.

Next, scroll down to just above section #8 in the update selector and add the following code:

    if(nextDrop < dropDelay*0.5)
    {
        // update object hint
        [objectHint setVisible:YES];
 
        // get object's width
        float w = nextObject.ccNode.contentSize.width;
 
        // and adjust the objectHint according to this
        [objectHint changeWidth:w];
        objectHint.position = ccp([nextObject physicsPosition].x * PTM_RATIO-w/2, 310);
    }
    else
    {
        [objectHint setVisible:NO];
    }

If the nextDrop is less than half of the dropDelay, you set the objectHint to visible and its width to the dropping object’s width. you also set its position centered below the object’s x coordinate.

Build and run, and check if the object hint appears below the position of the next drop.

One last addition – the theme music! Import SimpleAudioEngine.h at the beginning of GameLayer.mm, if you haven’t done so already:

    #import "SimpleAudioEngine.h"

Add the following lines to the end of the init selector. The music resources have already been added to the project:

    // music
    [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"tafi-maradi-loop.caf"];

Build and run. The current state is available as 6-CompleteGame.

Protecting your assets

contentprotection_1-1

iOS apps have one big disadvantage: It’s easy to steal the contents from an app. You don’t believe me?

Just open your user folder in finder and navigate to Music/iTunes/iTunes Media/Mobile Applications. There are all the .ipa files from the apps you own.

Now copy one and rename it from .ipa to .zip. Extract the files. Inside you find a folder payload containing the app itself. Right-click and select Show Package Contents. Bam! – there you are.

It is of course illegal to use this content without permission. But be realistic: What would happen if somebody uses your assets? Can you check all the zillions of apps in the store? And what would you do if you see your assets in another game? Sue somebody in a foreign country? Good luck!

TexturePacker 3.0.10 comes with a new feature called “ContentProtection”. TexturePacker partially encrypts the data – using a variation of the xxtea algorithm with 128-bit keys – protecting your spritesheets. The algorithm is designed to deliver good protection while having close to no overhead in runtime and memory. This works with a variant of Cocos2d’s pvr.ccz files.

All your need to do is three simple steps:

  1. Activate the encryption in TexturePacker
  2. Exchange 2 files in Cocos2d
  3. Set the key in your app

The encryption protects you against most of the people trying to steal your assets. It won’t protect you from a professional hacker. But encrypting the complete file with AES or even RSA won’t change that.

That’s because in the end the assets must be converted to something iPhone can use – and thus the key and algorithm both have to be part of your game – and can be extracted by somebody. My guess is that most people would look for easier prey if they see that the files are protected.

Encrypting the assets

Open the jungle.tps file in TexturePacker and click on the Content protection icon. As already said – a 128-bit key is required. You can create a key by pressing the Create new key button. Save as global key allows you to save the key and to use it with other sheets by simply restoring it using Use global key.

TexturePacker’s .pvr previewer also uses the global key to decrypt your images if you want to inspect them.

Now just press Publish.

Open background.tps and frame.tps, open ContentProtection, press Use global key and Publish.

Using the same key for all files should be fine – and it makes handling the assets way easier than applying an individual key to each file.

Adding decryption to Cocos2d

Simply replace ZipUtils.h and ZipUtils.m in libs/cocos2d/Support with the files from the ContentProtection folder in the MonkeyJump zip file.

Setting the key

The key must be set as four 32-bit values. It is separated in multiple steps to make it harder for a potential attacker to retrieve the key.

If your key is aaaaaaaabbbbbbbbccccccccdddddddd you have to split it into 4 parts, each consisting of 8 hex digits.
Make a call to caw_setkey_part with the index of the part and the value prefixed with 0x.

You can place this code as a block or as separate lines in different source code files. Just make sure that it gets executed before loading the first sprite sheet. The more it’s distributed – the harder the key is to hack.

If you want to keep it simple, copy it in the init selector in GameLayer.mm:

caw_setkey_part(0, 0xaaaaaaaa);
caw_setkey_part(1, 0xbbbbbbbb);
caw_setkey_part(2, 0xcccccccc);
caw_setkey_part(3, 0xdddddddd);

Import the ZipUtils.h in the file(s) that set the key – it defines caw_setkey_part

#import "ZipUtils.h"

The implementation with the key from the screenshot would look like this, you could place it in the init selector of the GameLayer.mm

caw_setkey_part(0, 0x50e23559);
caw_setkey_part(1, 0x293f9940);
caw_setkey_part(2, 0x3a4fb526);
caw_setkey_part(3, 0x592e48b2);

The loader code warns you if you entered a wrong key in debug build. A release build does not contain this code to make it more difficult to attack.

The final version is available as 7-Final.

Where to Go From Here?

If you don’t have it already, here is all of the source code for this tutorial series.

Congratulations – you finished the tutorial! Looking back, you’ve learned a ton of things:

  • Using TexturePacker to create your sprite sheets
  • Using PhysicsEditor to create your collision shapes
  • Building a physics-based game with collision detection and sound using Box2d
  • Handling different resolutions including iPhone 5
  • Building a HUD layer to display the health and score
  • Protecting your assets from being stolen

I hope you have enjoyed this tutorial! I’d love to hear what you think of the game and the products you used to make it, so keep the questions and comments coming.

Owner of CodeAndWeb Developer of TexturePacker and PhysicsEditor. Co-author of "Learn cocos2d game development with iOS 5"

User Comments

9 Comments

  • Download link is broken www.raywenderlich.com/downloads/monkeyjump2.zip please fix.
    jennismfma
  • Hi, I followed monkey jump tutorial and implemeted that game by my own.
    I also purchased TexturePacker and PhysicsEditor for my further development purpose.
    But I am stuck with the problem of "joint point" , as it is not yet implemeted with GBox2D.
    So isn't there any way by which I can implement it with GBOx2D.
    There might be some alternatives for this?
    can you help me in this?
    jd7
  • Can anyone tell me why I got the exception when I tried to run the sample projects?

    Here is the error
    *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Format not supported'
    *** First throw call stack:
    (0x25a0012 0x1b8ce7e 0x259fe78 0x1622665 0x933c 0x3593 0x58e15 0x3272 0x2bfd 0xace157 0xace747 0xacf94b 0xae0cb5 0xae1beb 0xad3698 0x24fbdf9 0x24fbad0 0x2515bf5 0x2515962 0x2546bb6 0x2545f44 0x2545e1b 0xacf17a 0xad0ffc 0x26b6 0x25e5)
    libc++abi.dylib: terminate called throwing an exception
    dennistung
  • Hello. First of all, thank you Andreas and Ray for the terrific tutorial.

    I just want to alert you to the fact that the source code from http://cdn1.raywenderlich.com/downloads/MonkeyJump2.zip appears to be missing the following files "shapes.plist" and "sounds.txt". The files are in the Resources folder (in Finder), but they need to be added to the project. Otherwise the app crashes in GameLayer.mm when it tries to access shapes.plist.

    @dennistung: You may want to try this fix for your exception.
    lookdev
  • dennistung wrote:Can anyone tell me why I got the exception when I tried to run the sample projects?

    Here is the error
    *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Format not supported'
    *** First throw call stack:
    (0x25a0012 0x1b8ce7e 0x259fe78 0x1622665 0x933c 0x3593 0x58e15 0x3272 0x2bfd 0xace157 0xace747 0xacf94b 0xae0cb5 0xae1beb 0xad3698 0x24fbdf9 0x24fbad0 0x2515bf5 0x2515962 0x2546bb6 0x2545f44 0x2545e1b 0xacf17a 0xad0ffc 0x26b6 0x25e5)
    libc++abi.dylib: terminate called throwing an exception




    Same problem here. Can someone help me?
    milo
  • Add the "shapes.plist" file to the project, in the resource group, and the game will run smoothly :)
    frederic
  • Ogle
    SongSu
  • frederic wrote:Add the "shapes.plist" file to the project, in the resource group, and the game will run smoothly :)


    Still not work in my case. I downloaded the source code and add the shapes.plist (or even sounds.txt), it still not work. and got the same proble : format not supported
    chenzhifu
  • Hi,


    First of thanks for the wonderful tutorial.

    Well I have purchased Physics Editor and Texture Packer, and they are working completely fine.
    Now, coming to the point.
    I am developing a game using GBox2D framework where we used GB2Sprite to create sprite's
    now the question here is

    1) I want to put multiple objects in one object say for example : I want to create a bucket and put some chocolates in it, so as a result when I change the position of bucket the chocolates in it automatically move with bucket.

    So, Approach I used here is I tried to put a GB2Sprite in another GB2Sprite. so that when I change the position of parent GB2Sprite then child will automatically move with the parent.

    But, Its not working, I am not able to move the child with parent event though I put child GB2Sprite in parent GB2Sprite and try to move it. Can you please let me know that How can I make it in working conditions or if its not implemented then suggest me some work arunds.

    another problem I am facing is:

    I want to use UIViewContoller's interface builder in Cocos2D + Box2d application to create some screens. I should be able to move from one UIViewController to another CCLayer by pressing a button of UIViewContoller . Is it possible? if yes then how?
    jd7

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

  • Kirill Muzykov
  • Matt Luedke

... 50 total!

Update Team

  • Andy Pereira

... 15 total!

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Przemysław Rembelski
  • Miguel Angel
  • Lin Ma

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!