显示标签为“Game”的博文。显示所有博文
显示标签为“Game”的博文。显示所有博文

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

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

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

In this tutorial, youll learn how to make a game about a monkey having a rough day. He’s just minding his own business, but these crazy objects keep falling from the sky!

To see what I mean, check out this cool video:


By the end of this tutorial, you’ll have created a cool physics enabled game, and learned how using TexturePacker and PhysicsEditor can save you a ton of development time.

To go through this tutorial, you should have some basic familiarity with Cocos2D. If you are new to Cocos2D, check out some of the other tutorials on this site first.

You will also need a copy of TexturePacker and PhysicsEditor. To to through this tutorial you can use an evaluation version from TexturePacker.com and physicseditor.de, but note that there are some features that are not present in the evaluation versions. If you want to order the full versions, here’s a direct link to order the bundle.

Ready to make that monkey jump? Keep reading to get started!



Getting Started


We will start with the design phase. So let me give you a brief overview of how the game will work.

As you’ve seen, our main character will be a monkey. His movement will be controlled with the accelerometer – that is, by tilting the iPhone or iPad left and right. The monkey will jump when the screen is tapped.

During the game, items will drop from the top of the screen, and the dropping frequency will increase the longer the game continues. We will have a drop indicator to show the player where the next item will fall. Items pile up, and the monkey must stay on top to stay alive.

There will be two sorts of items: those that hurt the monkey, and those that heal him – namely, bananas! The monkey’s health will be displayed in the top-left corner as a bar of bananas.

The score will be displayed in the top-right corner, and will be at the height of the highest item on the stack.



To get started, download the tutorial source code and unzip it into your preferred location. Your starting point will be 0-BaseProject, which contains the basic Cocos2d setup and some integrated assets.

I’ve added directories for several stages of the development process containing the results you should achieve. These directories will make your life easier in case you get lost, or if you simply want to skip one part of the project.

Each folder contains the complete project and is independent from the rest. Inside each folder are two more folders:

The first is called Assets, and contains such things as the TexturePacker and PhysicsEditor save files, a directory holding the Xcode project, and a free game art pack created by Vicki Wenderlich.

The Assets folder is organized further into the subfolders background, containing the background graphics, and jungle, containing the foreground objects and animation.

The second folder is called MonkeyJump. It contains the Xcode project and the sources, which are contained in a second MonkeyJump folder which has the following subfolders:

  • libs: Cocos2d, Box2d, Cocos Denshion… all the Cocos2d stuff
  • GBox2D: my ObjectiveC++ Box2d wrapper
  • Resources: sounds and music

Creating Sprite Sheets


Time to start the real work, beginning with sprite sheets.

Open the 0-BaseProject folder and have a look at the Assets folder. I’ve already arranged the images for us. We’re going to add the sprites from the “background” folder into one sprite sheet, and the sprites from the “jungle” folder into as second.

The background sheet will use RGB565, a 16-bit image format that reduces the amount of memory used by 50%. Using RGB565 will also speed up rending. The foreground sheet, with most of the content, will use RGBA8888 for full quality. For more information on pixel formats with TexturePacker, check out this tutorial.

Creating the Background Sprite Sheet

Let’s start by creating the background sheet. Start TexturePacker, navigate to the O-BaseProject\Assets\background folder, and drag it into the right hand side of the TexturePacker window. You’ll see the jungle background image show up in the center of the window.



To set up the parameters for the texture, first choose the Data format, which is of course Cocos2d. Next choose the Texture format. We will use zlib compressed PVR format .pvr.ccz.

TexturePacker warns us that we should turn on pre-multiplied alpha. Let’s agree to that and click “Apply.”

Pre-multiply means that all color values get multiplied by their transparency value when saving the file. This speeds up graphics rendering in the game, since the multiplication step does not need to be performed at runtime.

The main advantage of pvr.ccz is that it can be loaded quite fast, and it usually consumes less memory than PNG. Note in the bottom-right corner of TexturePacker how the memory consumption is reduced from 8192kB to 4096kB.

The quality of the image will suffer a bit: we can see that the gradients are now not as smooth and have some artifacts called banding. We can compensate for this by setting Dithering to FloydSteinberg.

The following two images show a part of the jungle. The left one has no dithering, while on the right one, dithering has been enabled:


RGB565 image with banding artifacts


RGB565 image with dithering


Next, set the Data file to background-hd.plist in the Resources directory of the Xcode project. This will automatically set the Texture file to background-hd.pvr.ccz.

Don’t worry that the paths are displayed as absolute paths. TexturePacker creates a relative path to the document file as soon as it is saved. This means we can move our project around as long as we don’t change the relative position of the assets and resources to the saved .tps file.

The -hd extension is important, because TexturePacker can save retina display and reduced resolution images for older devices. To enable this, simply check the AutoSD option. This will create background-hd.plist and background-hd.pvr.ccz files for retina display, and background.plist and background.pvr.ccz for reduced resolution.

Max. width and Max. height appear in red, which means TexturePacker can’t layout the sprite within the texture’s dimensions. This is because padding is set to 2 by default.

Scroll down on the left pane and set both border padding and shape padding to 0. Now all numbers should appear in black.



Finally, let’s save our sheet as background.tps in the Assets folder by first clicking Save and then Publish. This creates four files in the Resources folder: background-hd.plist, background-hd.pvr.ccz, background.plist and background.pvr.ccz. (Open the Resources folder to make sure the files are there.)

Creating the Jungle Sprite Sheet

Now create a new sheet for the foreground by pressing the New button on the toolbar. Drag the complete jungle folder from the Assets folder into the empty right pane.

See how TexturePacker builds the complete directory structure on the right pane? (You can expand the tree view by pressing the arrow button to the left of the word “jungle”.)

Folders in Texture Packer

We’re using a feature called Smart Folders, where TexturePacker rescans the directory every time we add sprites to the folder. If you want to add a new asset, or rename or change an existing asset, just drop it into the folder on your file system, and when you reenter TexturePacker it will automatically update the sheet.


Note: If you need to update many sprite sheets at once, you can switch to Terminal and update all .tps files from the command line by calling TexturePacker *.tps.

You can even integrate TexturePacker with your Xcode build process. This has several advantages.

First, you no longer need to worry about sprite sheets – just add the sprites to your Assets folder, and you’re done.

Second, if you use version control, sprite sheets will hang around as binary blobs. This will grow your working copy size pretty fast, especially when using Git or Mercurial for version control. The easy way around this is to simply not add the sprite sheets to version control, and instead create them on the fly.

For more information on integrating TexturePacker with Xcode 4, check out this tutorial.

For the options, we’ll use a nearly identical setup as for the background sheet (see screenshot below). Just make sure to leave the image format at RGBA8888 for full quality.



Also be sure to leave the paddings set to the default of 2. This will reduce artifacts on the sprite borders. Sometimes OpenGL drags in pixels from neighboring sprites. Padding avoids this problem by adding transparent borders around the sprites.


Note: If you want to use sprites as tiles with seamless connections, you must use either select extrude or reduce border artifacts, which replaces the transparent borders with colored ones. If you do not do this, you might get transparent lines between the tiles.


Set jungle-hd.plist in the Resources folder of the project as the Data file, and jungle-hd.pvr.ccz in the same folder as the Texture file. Check the box next to AutoSD.

Finally, save the sheet as “jungle.tps” in the Assets folder and click Publish. This creates four files in the Resources folder: jungle-hd.plist, jungle-hd.pvr.ccz, jungle.plist, and jungle.pvr.ccz. Check and see if they are there.



Creating Physics Shapes Manually


Next we’ll set up the physics shapes with PhysicsEditor!

Launch PhysicsEditor and start with the basic setup. We need to set the Exporter to Box2d Generic (PLIST). For this I added a loader which you can use in your Cocos2d projects. If you are not satisfied with the formats supported by PhysicsEditor, you can simply create your own custom data exporter.

Set the PTM-Ratio to 170. This tells Box2d that 170 pixels are equal to 1 meter (39.37 inches), the internal measurement unit of Box2d. I chose 170 pixels because this is the monkey’s height, and he is going to be 1 meter tall in the simulation.

We’re going to create all the physics shapes for the high-res sprites only. This is okay, since we want the physics parameters to be identical on retina display and old displays.

Creating different shapes for different screen resolutions would create different masses for each object, resulting in different physics behavior – which we don’t want! The idea is to run the same simulation on all devices and only adjust the visuals.



Setting Up the Fixture

We’re now going to create our first shape by hand. Drag the jungle/floor/grassfront.png shape onto the left pane of PhysicsEditor. When you do this, the shape will also appear in the center pane, which is the main editor area.

Note the small blue circle with the cross inside: that is the anchor point of the shape. We’re going to leave the anchor point for this shape where it is, on the bottom right corner. Later on, I’ll show you how to change the anchor point.

First use the polygon tool from the top tool bar. As soon as you click on it, a small red triangle appears. You can drag this triangle with your mouse.

The handles allow you to change the triangle’s shape. Double-clicking somewhere near a line will add an additional vertex. Double-click the vertex to remove it.

You do not have to care about things like polygon orientation or convex or concave shapes. This is all handled by PhysicsEditor.

Now create a rectangular shape for the floor. It should cover the soil but not the grass, and look something like this:



Setting Up the Parameters

Since the floor is going to be a static shape, its parameters are going to be very basic.

Density affects the weight of a body/fixture. Since the floor will not move, the value of the density is of no importance. Restitution is the degree a colliding shape will rebound from another object. For the floor, we want to keep this at 0.

Friction is the only important value here, as it determines how much the sprites will slide on the floor. Set this to 0.7.

The next items to set up are the collision bits. PhysicsEditor lets you handle these parameters quite easily – that is, without doing hex math. First, give the bits useful names by setting up the names in the text fields associated with the bits:

  • floor – both the floor and the walls
  • monkey – what else?
  • good_objects – objects that don’t hurt the monkey
  • bad_objects – objectw that hurt the monkey

Ignore the other bits – we don’t need them.



Two objects can collide only when Object B’s bit category is of a type that has been selected as a mask for Object A, and vice-versa. We will tick the Category field for the floor bit since this shape is a floor, and then the Mask field for all the bits. This enables collision between all objects and the floor.

Save the project as “shapes.pes” inside the Assets folder.

A Little Magic: Creating Shapes with the Tracer


Creating shapes by hand isn’t always fun. So let’s use the magic wand tool and let PhysicsEditor do the work for us!

Drop the following objects onto the left pane: backpack, banana, bananabunch, canteen, hat, pineapple, and statue.



Select the backpack, and click the magic wand icon on the tool bar. This will open a new window, the tracer. The tracer shows the current shape with an overlay of the traced shape.



The most important setting is the Tolerance. This value tells the tracer how exactly the polygon should match the shape. It directly influences the number of vertices the polygon will have. For example, setting the Tolerance to 20 will result in a 5-vertex polygon that no longer fits the shape; setting it to 1 creates a perfectly-matching polygon with 39 vertices.


Not enough vertexes


Too many vertexes


Good trace


A Tolerance value of 5 gives us an acceptable match, with about 15 vertices (the exact amount sometimes varies with the starting point of the tracer). That’s fine! Click OK to get back to the main screen. If you want you can fine-tune the shape, but don’t be too finicky!

Now set the backpack’s parameters. Set the Density to 5.00, the Restitution to 0.10 (so that the backpack bounces a little bit) and the Friction to 0.5. You might want to play around with these parameters later to get a better feel for how they work.

In the collisions section, tick the Cat. box next to bad_objects, and enable collision with all other objects by making sure all the Mask boxes are ticked.

Also drag the anchor point (the blue circle with the cross inside) to the center of the backpack shape. Alternatively, you can set the anchor point to the exact center of the shape by setting the Relative anchor point values in the right sidebar to 0.5 and 0.5.



Now that you know how to create shapes, either by hand or using the tracer, go ahead and do the remaining ones yourself! Use the parameters indicted in the table below. And don’t forget to drag the anchor point for each shape to the position indicated in the images below.












































































ObjectToleranceDensityRestitutionFrictionCategory (Cat.)Mask
backpack55.00.10.5bad_objectsfloor, monkey, bad_objects, good_objects
canteen53.00.10.2bad_objectsfloor, monkey, bad_objects, good_objects
hat51.00.150.4bad_objectsfloor, monkey, bad_objects, good_objects
statue510.00.050.7bad_objectsfloor, monkey, bad_objects, good_objects
pineapple51.00.40.7bad_objectsfloor, monkey, bad_objects, good_objects
banana51.00.50.5good_objectsfloor, monkey, bad_objects, good_objects
bananabunch51.00.30.7good_objectsfloor, monkey, bad_objects, good_objects



Last But Not Least: the Monkey


There’s one shape we have left to create, and that’s because it’s the most complex. Our monkey consists of quite a few animation phases (fourteen total):



The first idea that comes to mind is to add a collision polygon for each animation frame to make a perfect match for each phase of the animation. But this would be a bad idea.

First of all, the frequent exchange of fixtures would consume a good deal of CPU power. But the bigger problem is that with each new shape, the mass of the monkey would change!

This is because Box2d calculates mass based on a polygon’s area and density. If you change the polygon, the monkey will gain or lose weight! And this will lead to the physics simulation behaving erratically.

The solution is to make only one shape for the monkey, which we will try to make as good a fit as possible. To begin, drag one of the monkey shapes onto PhysicsEditor. You can rename the shape by double-clicking its name.



Now click the “+” button to add the other animation phases for the monkey. The files will show up in the combo box and you can switch between them.

Start the tracer by clicking on the magic wand icon. What you see now is that the shape does not fit the monkey anymore. This is exactly what we want.



The tracer is now in animation tracing mode. You can switch between two frame modes: intersection and union. Intersection creates the polygon only from the parts that are covered by all sprites, whereas union uses the parts that are covered by any sprite. Choose intersection.

The slider allows you to browse through the animation phases and see how the shape fits the different phases.

We will take the shape the tracer gave us just as an estimate. This is because we must split up monkey’s body into several parts. Here is what we have to build:



Sometimes polygon shapes don’t slide smoothly over other polygons and stick to each other. This is why we’re building the monkey from circle shapes.

The other reason is that using circles results in faster collision detection. The math to check if a point is inside a circle is much simpler than that for testing for a point inside a polygon. So we’ll save CPU power. We’ll delete the tracer shape after the other parts are set up.

First build the head of the monkey. Use a circle shape and make it as big as the traced version. The head must be treated differently from the other body parts, since we want the monkey to get hurt when objects land on his head from above.

To distinguish the head from the other parts, set the Id to “head.” The head’s category is monkey, and it collides with good_objects, bad_objects and floor, so make sure these are selected as masks. Set the density to 2.0, the restitution to 0.1, and the friction to 0.5.

Next build the body of the monkey from two circles, one covering the torso and one the legs. Set the Id to “body.” The body’s category is monkey, and it collides with good_objects, bad_objects and floor. Set the body’s density to 2.0, its restitution to 0.1, and its friction to 0.5.

Cycle through the different animation phases by selecting the different images from the Filename combo box. Check if the circles fit all phases. It doesn’t have to be a perfect fit, and you can come back to PhysicsEditor any time you want to tweak the collision set.

Finally, delete the polygon created with the tracer.

During the game we need to check if there are objects to the left or right to trigger the push animation. To do this, place two sensors on the left and right sides of the monkey.

Sensors are fixtures that don’t interact with other bodies and simply report collisions. To make a fixture a sensor, set isSensor.

Set the Id to “push_left” for the left sensor and “push_right” for the right one. The category is monkey, and they collide with bad_objects only – we don’t want the monkey push the floor and the bananas away!


Make the monkey shape fit all animation phases

One last thing to do: save and click Publish! When asked for a location, store the file in the Resources folder of the project as shapes.plist.

That’s it for now in PhysicsEditor. Next we move to Xcode and start coding our game!

The Xcode Project: Overview


I created the starter project using the standard Cocos2d+Box2d template that comes with Cocos2d and removing all the demo stuff. I also added the sound resources and some classes which will make your life easier when working with Box2d inside Cocos2d.

One important thing to know about this project is that all files must have the .mm extension instead of .m. This is because Box2d is C++ based, so we must use Objective-C++ for development.

Now let’s discuss the starter project in more detail:

GBox2D

First let me give you some information about GBox2D. GBox2D is an Objective-C wrapper for Box2d that I developed for my upcoming game TurtleTrigger.

These are the main classes in Gbox2D:

  • GB2Engine: This class wraps the Box2d world simulation. It runs the simulation, updates all sprite positions and rotations and can iterate through all objects in the world. It is implemented as a singleton class to make access as easy as possible.
  • GB2Node: This class combines a CCNode and a B2Body. It’s the glue between the physics simulation and the graphics representation inside Cococs2d. It also contains selectors for simple management of the physics object and implements a proxy to access the inner node’s data.
  • GB2Sprite: This class is derived from GB2Node and specializes in using CCSprite as the inner object.
  • GB2DebugDrawLayer: This is a Cocos2d layer that wraps the debug drawing. It can be added to your project like a normal layer. When added it will draw the physics shape outlines. The nice thing about it is that it detects when running on a retina display target and scales the content accordingly.
  • GB2Contact: This structure will be passed as parameter to an object when a collision is detected. Both objects involved in the collision will be called for each single point of contact.
  • GB2WorldContactListener: This is a C++ class that reacts to collisions in the physics simulation.

If you’re curious, go ahead and scan through these classes to get an idea of what they do. Don’t worry if you don’t understand what they’re doing – you’ll learn how to use it in the next few sections.

Collision Detection with Gbox2D

GBox2D makes collision detection a piece of cake! This is because you do not need to create one huge switch-case statement or a series of if-else cascades to detect the various possible collision combinations.

Instead, GBox2D simply uses the names of the colliding classes and calls selectors with names derived from the class names! If this sounds too abstract, take a look at the following example:

Let’s assume you have a monkey that is an object of class Monkey, and a banana that is an object of class Banana. If both objects begin to collide, the following selectors will be called by GBox2D:


[banana beginCollisionWithMonkey:collisionA];
[monkey beginCollisionWithBanana:collisionB];


If the collision is released, because the objects do not touch anymore:


[banana endCollisionWithMonkey:collisionA];
[monkey endCollisionWithBanana:collisionB];


The collisionA and collisionB parameters contain collision information, e.g., which objects and which fixtures took part in the collision. We will use this information to see if the monkey was hit on his head or body.

AppDelegate

Things I changed from the AppDelegate contained in the original Box2d project are as follows:

First I set the default pixel format of the frame buffer to RGBA8. That means that the game gets the full 24-bit color depth. I disabled the depth buffer, since we don’t need it.


EAGLView *glView = [EAGLView viewWithFrame:[window bounds]
pixelFormat:kEAGLColorFormatRGBA8
depthFormat:0
];


The next important thing to set is pre-multiplied alpha. This is because we use pre-multiplied PVR images created with TexturePacker. If we don’t set pre-multiplied alpha, our images will have dark borders.


[CCTexture2D PVRImagesHavePremultipliedAlpha:YES];


Since we’re going to use the random generator to select objects to drop, we have to seed it. The best way to do this is using time(). If you forget to seed the random generator, you will still get random numbers – but they will be the same with every start of the game.


srand (time (NULL));


When the initialization is done, the GameLayer scene is started:


[[CCDirector sharedDirector] runWithScene: [GameLayer scene]];


GameLayer

The GameLayer is a simple class derived from CCLayer. In this state it consists of an empty init function…


-(id) init
{
if( (self=[super init]))
{
}
return self;
}


…and a static selector that wraps the CCLayer into a CCScene to hand over to the Director:


+(CCScene *) scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];

// 'layer' is an autorelease object.
GameLayer *layer = [GameLayer node];

// add layer as a child to scene
[scene addChild: layer];

// return the scene
return scene;
}


Audio Resources

I got the theme music, tafi-maradi-loop.caf, from http://incompetech.com/

Since I knew I wanted to loop the music, I made things easier on myself and chose a theme without a vocal track.

The sounds effects for the objects were obtained from http://soundbible.com (thanks to Mike Koenig). Some of them were created using cfxr.

All sounds and the music files were converted to caf format. See Ray’s Audio 101 tutorial for more information.

If you compile and run the project, you will simply see a black screen. So let’s add some content to that screen now!

Basic Setup in Xcode


The goals for this section of the tutorial are to set up the basic game layers and backgrounds, and to set up the physics engine.

Before we dive into the code, there’s one more thing to do: add the resources we created in PhysicsEditor. Remember these?

  • background.plist
  • background.pvr.ccz
  • background-hd.plist
  • background-hd.pvr.ccz
  • jungle.plist
  • jungle.pvr.ccz
  • jungle-hd.plist
  • jungle-hd.pvr.ccz
  • shapes.plist

Add the above files to the Resources folder in your Xcode project by Control-clicking on the Resources folder inside Xcode, selecting Add Files To “MonekyJump” and then selecting the files listed above from the project Resourcess folder.

Now for the code. First, we need to create a Floor class to represent the floor for the game. Add Floor.h and Floor.mm files to your project by creating a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Floor, and make it a subclass of GB2Sprite. And don’t forget to change the extension for Floor.m to .mm once it has been created.

Floor.h simply contains this:


#pragma once

#import "cocos2d.h"
#import "GB2Sprite.h"

@interface Floor : GB2Sprite
{
}

+(Floor*) floorSprite;

@end


And Floor.mm this:


#import "Floor.h"

@implementation Floor

+(Floor*) floorSprite
{
return [[[self alloc] initWithStaticBody:@"grassfront" spriteFrameName:@"floor/grassfront.png"] autorelease];
}

@end


The only remarkable line of code here is the call to the initWithStaticBody selector. This makes our object a static object – one that isn’t moved by the physics engine. It initializes the body’s shape using a shape from the shapes.plist file with the name “grassfront.”

It also uses a sprite image with the name floor/grassfront.png that is taken from jungle.plist.

Why did we derive this class from GB2Sprite instead of simply using a GB2Sprite directly? The answer is GBox2D’s collision handling, which uses the name of the class to call appropriate selectors on the colliding objects. Since we want to know when something collides with the floor, the class name for the floor object must be distinguishable from other GB2Sprite objects.

The next thing to do is update the GameLayer. Add some instance variables to hold the required objects in GameLayer.h:


@interface GameLayer : CCLayer
{
CCSprite *background;                   // weak reference
CCSprite *floorBackground;              // weak reference
CCSpriteBatchNode* objectLayer;         // weak reference
}


We will store the objects as weak references – that is, without increasing the retain count for each object.

You do not need to worry about the CCSprites being deleted. They will be added as children of the CCLayer and thus have a retain count of at least 1. This is done because otherwise we will be caught in a retain cycle and not able to free the memory allocated by these objects.

Now fill GameLayer.mm‘s init selector with content. First, load the sprite sheets:


[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"jungle.plist"];
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"background.plist"];


Next, load the physics shapes into the GB2ShapeCache:


[[GB2ShapeCache sharedShapeCache] addShapesWithFile:@"shapes.plist"];


Then, set up the layers. We’ll divide the game into the following layers:

  • Background layer: The jungle image
  • Floor background: A single sprite with the tall grass
  • Object layer: Contains all the items and the monkey
  • Debug draw layer Activated as needed
  • Hud layer Has the score and live energy indicators (to be added later)



Add the code to create the basic background and floor background layers to init:


// Setup background layer
background = [CCSprite spriteWithSpriteFrameName:@"jungle.png"];
[self addChild:background z:0];
background.anchorPoint = ccp(0,0);
background.position = ccp(0,0);

// Setup floor background
floorBackground = [CCSprite spriteWithSpriteFrameName:@"floor/grassbehind.png"];
[self addChild:floorBackground z:1];
floorBackground.anchorPoint = ccp(0,0);
floorBackground.position = ccp(0,0);


Then, add the object layer. This will be a sprite batch node to speed up rendering of the objects:


objectLayer = [CCSpriteBatchNode batchNodeWithFile:@"jungle.pvr.ccz" capacity:150];
[self addChild:objectLayer z:10];


And finally, the debug draw layer:


// add the debug draw layer, uncomment this if something strange happens ;)
[self addChild:[[GB2DebugDrawLayer alloc] init] z:30];


If you want to disable the debug drawing, simply comment out the second line. If enabled, the physics shapes will be drawn over the sprites, allowing you to see where collisions happen and if all shapes are properly aligned.

Next, add the floor object as the child of the object layer. Include Floor.h at the top of GameLayer.mm:


#import "Floor.h"


Then add the floor object at the end of init:


[objectLayer addChild:[[Floor floorSprite] ccNode] z:20];


There isn’t anything more we have to do to add the objects to the physics world – everything else is covered inside Gbox2D!

Compile and run the project in the iPhone simulator. (By the way, the project in its current state is available in the folder called 2-BasicLayerSetup.) You should see something similar to this:



Nice – now let’s add some action to the game!

Dropping Objects


We have two related goals for this section of the tutorial: make our objects drop from the sky and add our sound effects.

Our base class for all the dropping objects will be called Object. It will handle the sound and some basic collision detection. We will derive other sub-classes later on in the tutorial from the Object class.

First, create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Object, and make it a subclass of GB2Sprite. (And remember to change the extension for Object.m to .mm)

Object is a simple class, derived from GB2Sprite. This means that it comes with physics and graphical capabilities built in.

To make our lives easier, I’ve named the sound files in the same way as the physics sprites and the images from the sprite sheet. This allows us to simply use the object’s name to create the right shape and sound when needed. You’re welcome!

In order for this to work, we need a property named objName – objName is passed into the initWithObject selector and stored as part of the class.

RandomObject is a factory method that creates a random object and hands over the right object name upon creation.

Paste this code into Object.h:


#pragma once

#import "cocos2d.h"
#import "GB2Sprite.h"

@interface Object : GB2Sprite
{
NSString *objName; // type of the object
}

@property (retain, nonatomic) NSString *objName;

-(id) initWithObject:(NSString*)objName;
+(Object*) randomObject;

@end


Let’s now go to Object.mm. Start with some needed imports and with synthesizing the objName property.


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

@implementation Object

@synthesize objName;


GMath.h contains some helper functions – for example, gFloatRand, a ranged floating point random number generation.

Next, add the init selector and instantiate the physics object. You can use the object’s name as it is to instantiate the physics shape. For the sprite frame name, you’ll need to add the folder’s name (which is object) and the .png extension. Store the objName in the property – we’ll need it during collision detection to play the sound effect.


-(id) initWithObject:(NSString*)theObjName
{
self = [super initWithDynamicBody:theObjName
spriteFrameName:[NSString stringWithFormat:@"objects/%@.png", theObjName]];
if(self)
{
self.objName = theObjName;
}
return self;
}


In the dealloc selector, simply release the objName property and call super dealloc:


-(void) dealloc
{
[objName release];
[super dealloc];
}


The next thing to add is our static factory method, which will simply create a random object. I decided to use a simple switch-case statement for this. The reason is that we’ll need to create special classes for banana and banana bunch later on. These two objects get only one case entry, while the other objects get three each, so that there’s a higher probability they appear more often.

Switch case constructs are quite efficient (usually implemented by the compiler using a jump table). You might save some CPU cycles by using an array with the names instead, but since the routine will be called once in a second, our way is fine.


+(Object*) randomObject
{
NSString *objName;
switch(rand() % 18)
{
case 0:
objName = @"banana";
break;

case 1:
objName = @"bananabunch";
break;

case 2: case 3: case 5:
objName = @"backpack";
break;

case 6: case 7: case 8:
objName = @"canteen";
break;

case 9: case 10: case 11:
objName = @"hat";
break;

case 12: case 13: case 14:
objName = @"statue";
break;

default:
objName = @"pineapple";
break;
}
return [[[self alloc] initWithObject:objName] autorelease];
}


Finally, add the closing end to the file:


@end


Now switch to GameLayer.h and add a forward declaration for the object class, directly after the #import statement:


#import "cocos2d.h"
@class Object;


Add these new members to the GameLayer class:


ccTime nextDrop;    // Will keep the time until the next drop.
ccTime dropDelay;     // The delay between two drops.
Object *nextObject;   // Contains a reference to the next item to drop.


Switch to GameLayer.mm and add an import of Object.h to the imports at the start of the file. Also import GMath.h:


#import "Object.h"
#import "GMath.h"


Initialize the new variables at the end of the init selector, and schedule an update selector with every frame update:


nextDrop = 3.0f;  // drop first object after 3s
dropDelay = 2.0f; // drop next object after 1s                

[self scheduleUpdate];


The last line will call a selector called “update” for every frame. The parameter to this selector is the time elapsed since the selector was last called. Add the update method right after init:


-(void) update: (ccTime) dt
{
// 1 - drop next item
nextDrop -= dt;
if(nextDrop <= 0)
{
// 2 - do we have the next object?
if(nextObject)
{
// 3 - set the object as active, making it drop
[nextObject setActive:YES];

// 4 - set next drop time
nextDrop = dropDelay;
// reduce delay to the drop after this
// this will increase game difficulty
dropDelay *= 0.98f;           

}

// 5 - create new random object
nextObject = [Object randomObject];
// but keep it disabled
[nextObject setActive:NO];

// 6 - set random position
float xPos = gFloatRand(40,440);
float yPos = 400;
[nextObject setPhysicsPosition:b2Vec2FromCC(xPos, yPos)];

// 7 - add it to our object layer
[objectLayer addChild:[nextObject ccNode]];
}
}


Let’s go through the above code section by section.

  1. This section simply reduces the time interval since update was last called from nextDrop. If nextDrop falls below 0, it’s time to create a new item to drop.
  2. If the nextDrop timer runs out, this section checks if there is already an object stored in nextObject.
  3. If so, it’s set to to active in here. Setting the object to active gives the physics engine control over the object.
  4. This section sets the time until the next drop to the current drop delay, and reduces the drop delay by 2%, making the game a bit more difficult with each dropped item.
  5. This section creates a new object to drop using our factory method in Object – randomObject – and sets the object to inactive, which keeps the object from dropping and participating in the physics simulation.
  6. This section gives the object a random position. The screen is 480pt wide (Cocos2d uses points as the base unit, with 1 pt equivalent to 1 pixel on “old” devices and 2 pixels on retina display devices). The code ensures that the object’s position is somewhere between 40 and 440 points. The section also sets the y-coordinate to 400 for the starting position so that the object will start offscreen from the top of the screen. The b2Vec2FromCC method is used to create a box2db2Vec2 from the point coordinates. B2Vec2FromCC transforms Cocos2d’s points to Box2d’s meters-based values.
  7. Finally, this section adds the object to the object layer.

Compile and run! You should see something similar to the following but of course, with different items. The items look a bit blurry since debug drawing is still enabled:



Disable the debug draw layer as follows by commenting out the relevant line in GameLayer.mm:


//        [self addChild:[[GB2DebugDrawLayer alloc] init] z:30];


Now your game should look much nicer:



Notice how the items can tumble out-of-screen to the left and right? The goal of the game is to let the items pile up, so we need to add a wall on each side of the screen.

To do this, simply create two new GB2Node objects. They will be out of the screen to the left and right.

Since GB2Nodes add themselves to the current physics simulation, you don’t need to add them manually. They are not represented graphically, so creating them will suffice.

Add these lines to the init in GameLayer.mm, right after the floor layer:


GB2Node *leftWall = [[GB2Node alloc] initWithStaticBody:nil node:nil];
[leftWall addEdgeFrom:b2Vec2FromCC(0, 0) to:b2Vec2FromCC(0, 10000)];

GB2Node *rightWall = [[GB2Node alloc] initWithStaticBody:nil node:nil];
[rightWall addEdgeFrom:b2Vec2FromCC(480, 0) to:b2Vec2FromCC(480, 10000)];


Compile and run. See how the objects are now kept inside the screen by our walls?



This looks nice, but there is still something missing. I think the objects should make some noise when colliding with each other. Don’t you agree?

I don’t want the objects to make sounds all the time, just when they hit each other at a decent speed. So we’ll check the object’s velocity, and play a sound only when it collides at a fast enough speed.

Add this code to Object.mm:


-(void) beginContactWithObject:(GB2Contact*)contact
{
b2Vec2 velocity = [self linearVelocity];

// play the sound only when the impact is high
if(velocity.LengthSquared() > 3.0)
{
// play the item hit sound
// pan it depending on the position of the collision
// add some randomness to the pitch
[[SimpleAudioEngine sharedEngine] playEffect:[NSString stringWithFormat:@"%@.caf", objName]
pitch:gFloatRand(0.8,1.2)
pan:(self.ccNode.position.x-240.0f) / 240.0f
gain:1.0 ];   

}
}


The above method must be named beginContactWithObject so that it will be automatically called by GBox2D each time two objects collide.

The linearVelocity method gives us the velocity of the object. Calling Length or LengthSquared on the object delivers the velocity’s value. I prefer using LengthSquared when comparing with a constant value, since it doesn’t require calculating the square root of the value.

We’ll play the sound with a call to SimpleAudioEngine’s playEffect method. The first parameter is the name of the audio file.

Remember that to make our lives easier I gave the sound effects the same name as the objects and sprites. So you can use the objName we stored earlier to get the right sound file. Use NSString to append .caf to the name.

Add some variation to the pitch by using gFloatRand with 0.8 and 1.2. This will play the sound with some pitch variation. It would be boring if every object made the same sound all the time.

The last trick to apply is to pan the sound’s source to the position of the object. Pan allows values between -1.0 and 1.0. The object’s x position (in points) will be somewhere between 0 and 480, so subtracting 240 and dividing by 240 will deliver that range.

If you want objects to make a sound when they hit the floor without rewriting a lot of the code, add the following method which forwards the object-floor collision to the object-object collision to Object.mm:


-(void) beginContactWithFloor:(GB2Contact*)contact
{
[self beginContactWithObject:contact];
}


Compile, run and see how the objects drop and make a sound upon collision.

Ah, but there is one more thing I don’t like about our game right now. The first item drops and then pauses in mid-air while the sound engine is initialized.

This won’t be a problem once we add the theme music, since the music will initialize the sound engine right away. But if you want to fix this now, first add an import statement to the top of GameLayer.mm:


#import "SimpleAudioEngine.h"


Then, add a call to SimpleAudioEngine’s shared object inside GameLayer’s init selector:


[SimpleAudioEngine sharedEngine];


The above implementation plays the same basic sound (with pitch variations) for all falling object collisions. If you’re ambitious, you could play different sounds depending upon the types of objects colliding. That is, play one sound when a canteen hits a canteen, and another when a banana hits a canteen…

Another way to improve this code would be to vary the gain of the effect with the speed of the collision.

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 One of the MonkeyJump tutorial! The project in its current form is available in the source code zip in the folder called 3-DroppingObjects.

Stay tuned for Part Two, where we’ll add our hero (the monkey), make him move and interact with objects – even make him teleport! Plus injury and death, life-giving bananas, a drop warning light… there’s lots of exciting stuff ahead.

Before we get there, let me know if you have any questions or comments about what we’ve done so far. I’ll be following the discussions in the forum 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 1 is a post from: Ray Wenderlich