2012年2月7日星期二

How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 3

How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 3:

This is a post by special contributor Andreas Loew, the creator of TexturePacker and PhysicsEditor.


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, we are creating a fun vertical scrolling platformer with Cocos2D, TexturePacker and PhysicsEditor.


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


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


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


We’ll be starting with the project where we 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!


Playing our 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, we’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. We will use this feature to improve performance.


GB2Engine has an iterate method that can be used to iterate all objects with a block. We 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];
}
}
}
];


Compile and test. 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 pruneDistance 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))
{...}


Compile and test. 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, we must be sure that the monkey teleports above all 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. Aadd 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
}


Compile 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, we 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 Our 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, we 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, we’ll simply delete all the monkey’s collision flags except for the floor. This will make the monkey fall dead on the floor. We’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;
}

...


Compile 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, we simply remove all objects from the GB2Engine and replace the current scene with a new GameLayer.


Compile 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 we can keep track of the monkey’s health.


We’re going to represent the monkey’s health with 10 banana icons. Each banana represent 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 we 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 we will need later) and the sprite representing each point of the monkey’s health. We 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, we initialize the HUD’s CCSpriteBatchNode super class with the sprite sheet.


Then, we iterate through the number of health tokens and create sprites for each of the bananas. We 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, we 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 we’re going to extend this code later and so will have that as two separate loops.


Next we 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];


Compile and test. 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);


Compile and test. 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, we check if the banana is already visible. If it’s not, we 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, we’ll do nothing since an action might already be running on it.


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


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


Compile 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 we 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. We 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, we use the CCSpriteFrameCache and request the frame for each digit. We’ll store the frame data in the digitFrame array. Then we 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];


Compile and test. 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, we’ll create a subclass of Object called ConsumableObject. This class gets a bool variable that keeps track as to whether the object was already 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 comsumed 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 was already consumed. If it wasn’t consumed, then scale the object to 0 and fade it out, and finally, delete the object from the game.


To do this, we 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, we simply add the new health value, ensuring that it does not exceed the maximum. Just setting the health is enough as the HUD take care of animating the health bar.


We’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.


Compile 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 we use in Object.mm does not yet create our 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:
...


Compile and test. 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];
}


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



Final improvements


Our game is looking awesome! But I still have a few more improvements for you.


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, we’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;


We create a semi-transparent red box as the drop indicator and set the box dimensions to 10×10 pixels. We’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, we set the objectHint to visible and its width to the dropping object’s width. We also set its position centered below the object’s x coordinate.


Compile and run! 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"];


Compile and run.


The final version of this project is in the folder called 7-done.


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

  • Building a HUD layer to display the health and score


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


This is a post by special contributor Andreas Loew, the creator of TexturePacker and Physics Editor.


How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 3 is a post from: Ray Wenderlich

2012年2月4日星期六

How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 2

How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 2:
This is a post by special contributor Andreas Loew, the creator of TexturePacker and PhysicsEditor.

Create this vertical scrolling platformer with Cocos2D!
Create this vertical scrolling platformer with Cocos2D!

Welcome back to the Monkey Jump tutorial! In this series, we are creating a fun vertical scrolling platformer with Cocos2D, TexturePacker and PhysicsEditor.

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

Before we stopped for a break, we had all of our 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, we will add our hero to the game, make him move and jump, and start adding some gameplay.

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

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



Getting Started


We 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: he’ll put up his hands when something drops from above, push items, jump, etc. 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, we initialize the monkey. The monkey’s movement will be affected by the physics engine, so make him a dynamic object. We’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 we 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 our game we’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 we want.

Do the Monkey Walk


Our 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, we 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, we’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 we 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 its tilted sharply.

Scaling the impulse with the object’s mass frees us from having to worry about the movement changing when we change the object’s shape in PhysicsEditor. If we didn’t scale the impulse and later we 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.

We 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, we 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, we 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. We’ll respond to velocity changes via 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 about – nice!

Slow Down Little Fella

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

We 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 addtion 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 thinks this looks much better now.



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

Jump, Jump!


Now, let’s make the monkey jump. For this, we need to make the game detect touch events anywhere on the GameLayer, because we want the monkey to jump at every touch.

Open GameLayer.mm and enable touch detection by adding the following line to the init selector:


// enable touches
self.isTouchEnabled = YES;


Also add the following selector at the end of the file (but before the @end marker). It forwards the touches to the monkey object via the jump method.


-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[monkey jump];   
}


You might notice at this point that we don’t have a jump method in the Monkey class. That’s what we’ll add next. Switch to Monkey.h and add the following method definition just before @end:


-(void) jump;


Now open Monkey.mm and add the following code before the @end marker:


-(void) jump
{
[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE)
point:[self worldCenter]];  

[[SimpleAudioEngine sharedEngine] playEffect:@"jump.caf"
pitch:gFloatRand(0.8,1.2)
pan:(self.ccNode.position.x-240.0f) / 240.0f
gain:1.0 ];
}


All the above code does is to apply an impulse to make the monkey jump and play the jump sound. We’ll pan the sound as we did for the object sounds in Part One of this tutorial.

Compile and run. Tap the screen to make the monkey jump.

Outta Sight!

But this is far from perfect: if you tap the screen multiple times, the monkey goes through the roof! Worse still, the camera does not follow the monkey.

Let’s fix the camera first. First, add the following code to the top of the update selector, above the existing code, in GameLayer.mm.


// 0 - monkey's position
float mY = [monkey physicsPosition].y * PTM_RATIO;


The above code copies the monkey’s y-position into a new variable mY. Of course, we could also access the monkey’s ccNode and take the y-coordinate from there. The end result will be the same as multiplying his physics position by the PTM_RATIO.

Now, add these lines to the end of the update selector after the closing curly brace after section #7.


// 8 - Adjust camera
const float monkeyHeight = 70.0f;
const float screenHeight = 320.0f;
float cY = mY - monkeyHeight - screenHeight/2.0f;
if(cY < 0)
{
cY = 0;
}


Here, we calculate a good value for the camera’s y-coordinate, one that will more or less center the monkey on the middle of the screen. Clamp the value so that it does not go below 0 so that the camera does not move below ground level.

Now let’s implement parallax scrolling of the background so that the monkey’s movement appears more natural. The effect is quite easy to accomplish: just multiply the background layer by a factor below 1.0 and set the position for the layers. This will make the background layer scroll slower. The further away a layer is from the camera, the smaller the factor must be.

Add this code below the last few lines added to update:


// 9 - Do some parallax scrolling
[objectLayer setPosition:ccp(0,-cY)];
[floorBackground setPosition:ccp(0,-cY*0.8)]; // move floor background slower
[background setPosition:ccp(0,-cY*0.6)];      // move main background even slower


Feel free to adjust the values to your liking.

That’s it – compile and run. Tap the screen multiple times to see the monkey rise. And beware of flying monkeys!!!!




And It All Piles Up!

If you play the game for a while, you’ll notice that the items continue to drop from the same fixed height – sometimes way below the monkey’s current position. In fact, after some time, items don’t even drop because they’ve piled up beyond the spawning point.

Fix this by changing the following line in GameLayer.mm‘s update method:


float yPos = 400;


Change the above line to this:


float yPos = 400 + mY;


Items will now spawn 400pt above the monkey’s head, wherever it may be.

Back to the monkey. You’ll have noticed that the monkey can actually jump again and again while up in the air. This just won’t do. We need to fix it so the monkey only jumps when he has contact with the floor.

Let’s start by counting the number of contacts the monkey makes with the floor.

Add a new variable to Monkey.h:


int numFloorContacts;     // number of floor contacts


Switch to Monkey.mm and add the following two new collision detection handlers to the end of the file (but above the @end marker):


-(void) beginContactWithFloor:(GB2Contact*)contact
{
numFloorContacts++;
}

-(void) endContactWithFloor:(GB2Contact*)contact
{
numFloorContacts--;
}


As the names imply, the first one detects the beginning of a contact/collision with the floor and the second the end of a collision. In the beginContact selector, we increase the value of the floor contact variable, and then decrease it in the endContact selector.

These selectors are going to get called by GBox2D every time a contact starts or ends between the monkey and the floor. (Remember: we created a separate class for the floor so that GBox2D now can call the appropriate selector with the class’s name).

Now, if the monkey is standing on the floor, then the numFloorContacts value should be at least one. Use this to our advantages by wrapping the code in the jump method in Monkey.mm with an if condition to see if the monkey is actually standing on the floor before jumping:


-(void) jump
{
if(numFloorContacts > 0)
{
[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE)
point:[self worldCenter]];  

...
}
}


Compile and run. Everything seems fine. Well… except that when the monkey lands on an object, he loses the ability to jump. To fix this, we’re going to consider any contact the monkey has with objects similar to a contact with the floor.

And it’s very simple to implement. Simply add a couple more collision-handling routines to the end of Monkey.mm and count object contacts the same way we counted floor contacts:


-(void) beginContactWithObject:(GB2Contact*)contact
{
// count object contacts as floor contacts 
numFloorContacts++;       
}

-(void) endContactWithObject:(GB2Contact*)contact
{
// count object contacts as floor contacts 
numFloorContacts--;       
}


Compile and run. Isn’t it much better now? And our game is already playable!

Push It!


Let’s improve the gameplay and allow the monkey to push objects to the left and right – and give him the ability to put his hands above his head to shield him from dropping objects.

Do you remember how you added the sensors to the left and right sides of the monkey in Part One? They come into play now! The key to the sensors was setting up the “Id” parameter in PhysicsEditor. You are going to retrieve this value now!

But before we do that, we need to add a few instance variables to keep count of the left and right sensors, as well as the number of contacts made with the monkey’s head. Add these variables to Monkey.h:


int numPushLeftContacts;
int numPushRightContacts;
int numHeadContacts;


Next, the beginContactWith* and endContactWith* selectors have a contact parameter we can use to determine which part of the monkey has contact with an object – the “Id” value we added in PhysicsEditor is stored as user data in each fixture. So replace the existing object contact handlers for Monkey.mm with the following:


-(void) beginContactWithObject:(GB2Contact*)contact
{
NSString *fixtureId = (NSString *)contact.ownFixture->GetUserData();
if([fixtureId isEqualToString:@"push_left"])
{
numPushLeftContacts++;
}
else if([fixtureId isEqualToString:@"push_right"])
{
numPushRightContacts++;
}
else if([fixtureId isEqualToString:@"head"])
{
numHeadContacts++;
}
else
{
// count others as floor contacts 
numFloorContacts++;       
}
}

-(void) endContactWithObject:(GB2Contact*)contact
{
NSString *fixtureId = (NSString *)contact.ownFixture->GetUserData();
if([fixtureId isEqualToString:@"push_left"])
{
numPushLeftContacts--;
}
else if([fixtureId isEqualToString:@"push_right"])
{
numPushRightContacts--;
}
else if([fixtureId isEqualToString:@"head"])
{
numHeadContacts--;
}
else
{
// count others as floor contacts 
numFloorContacts--;       
}
}


As you see from the new code, you retrieve the Id, which we’ll call fixtureId here, by accessing the contact parameter’s fixture and then accessing the fixture’s user data via the GetUserData method.

Now that we’re tracking the contacts, we can update the monkey’s animation frames to handle additional events.

Here’s the decision table for the various animations:




























Monkey

Objects

Animation
StandingNo object above headIdle
StandingObject above headArms up
MovingNo push sensor active in the directionWalk
MovingPush sensor active in the directionPush

Using the above table, we modify section #6 of updateCCFromPhysics in Monkey.mm as follows:


// 6 - Update animation phase
const float standingLimit = 0.1;
NSString *frameName = nil;
if((vX > -standingLimit) && (vX < standingLimit))
{
if(numHeadContacts > 0)
{
// Standing, object above head
frameName = [NSString stringWithFormat:@"monkey/arms_up.png"];                       
}
else
{
// Just standing
frameName = [NSString stringWithFormat:@"monkey/idle/2.png"];           
}
}
else
{
if(numFloorContacts == 0)
{
// Jumping, in air
frameName = [NSString stringWithFormat:@"monkey/jump/%@.png", dir];
}
else
{
// Determine if monkey is pushing an item
bool isPushing =  (isLeft && (numPushLeftContacts > 0))
|| (!isLeft && (numPushRightContacts > 0));

// On the floor
NSString *action = isPushing ? @"push" : @"walk";

frameName = [NSString stringWithFormat:@"monkey/%@/%@_%d.png", action, dir, animPhase];       
}       
}


Compile and test. Perfect! The monkey now behaves just as we want.



It Takes a Strong Monkey …

Playing a little bit more though, I think the monkey should be a bit stronger under certain conditions. Currently, he’s too weak to break free when an object is above his head. Let’s give him some extra strength when he’s trapped like that and wants to jump.

This is the current line from the jump selector in Monkey.mm that makes the monkey jump:


[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE)
point:[self worldCenter]];


Replace it with:


float impulseFactor = 1.0;

// if there is something above monkey's head make the push stronger
if(numHeadContacts > 0)
{
impulseFactor = 2.5;
}
[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE*impulseFactor)
point:[self worldCenter]];


That’s our monkey on steroids! He now uses a 2.5-times stronger impulse when there’s an object resting above him – this should allow him to break free of most of the objects.

Let’s also change the walking impulse in case the monkey needs to push an object to the side to break free. Go to updateCCFromPhysics and cut the following lines from section #6:


// Determine if monkey is pushing an item
bool isPushing =  (isLeft && (numPushLeftContacts > 0))
|| (!isLeft && (numPushRightContacts > 0));


Now paste that code into section #4 and modify it as follows:


// 4 - Determine direction of the monkey
bool isLeft = (direction < 0);

// Determine if monkey is pushing an item
bool isPushing =  (isLeft && (numPushLeftContacts > 0))
|| (!isLeft && (numPushRightContacts > 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);      
if(isPushing)
{
impulse *= 2.5;
}
[self applyLinearImpulse:-b2Vec2(impulse,0) point:[self worldCenter]];       
}


Compile and test – that’s much better. But there’s still a problem: when the monkey slightly grazes an object with his head, the new jump power makes him go through the roof!

We need to clamp his maximum speed inside the updateCCFromPhysics method. Add this to the end of section #3 of the updateCCFromPhysics method:


const float maxVelocity = 5.0;
float v = velocity.Length();
if(v > maxVelocity)
{
[self setLinearVelocity:maxVelocity/v*velocity];
}


Notice that in the code above we are directly modifying values controlled by the Box2d engine, thus affecting the overall behaviour of the physics engine. You should try to avoid doing this kind of manipulation.

Compile and test. I like the monkey’s behavior now. He reacts quickly, and is strong enough to push objects but does not become uncontrollable.

Where To Go From Here?


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

You’ve now reached the end of Part Two of the MonkeyJump tutorial! The project in its current form is available in the source code zip in the folder called 5-MonkeyJumpAndRun.

Stay tuned for the final part of the series, where we’ll add some performance improvements, add a HUD layer to the game, and yes – kill the monkey! :]

In the meantime, if you have any questions or comments, please join the forum discussion below!

This is a post by special contributor Andreas Loew, the creator of TexturePacker and Physics Editor.

How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 2 is a post from: Ray Wenderlich