Mar 172011
 

I received a few requests for a simple Mario-like platformer tutorial. So I am keeping my promise. The sample is far from a full fledged Mario clone, but I hope it it can at least start you off in the right direction. This tutorial builds upon concepts covered in “Beginning Box2d Physics Engine” and “Side Scrolling the Background in Box2d and Cocos2d”, so I suggest reading them first.

What’s Covered?

I will be covering the following topics in this tutorial.

  • Simple object oriented game design
  • User input / touch detection
  • Applying forces to game objects to make it move and jump
  • Collision detection

Simple Object Oriented Game Design

The example app will be a very rudimentary platformer, with some platforms and the player represented by a circle. Again, I am creating the physics world by reading in the tile map designed in the program Tiled as described in my previous tutorial. The newest version of Tiled at the time of this writing is 0.6.0, and you must go into preferences and set gzip as the default compression or Cocos2d will fail to load the tile map file. The player will be able to move to the right (moving to the left is left to the reader as an exercise) and jump. The app has only two objects. The first is the player object and the other is the platform object. Both the player and the platform object will inherit from the general GameObject class. The general GameObject class inherits from Cocos2d’s CCSprite object and has a property called “type” which is used to distinguish between a player and a platform. Having our objects inherit from the same parent object (GameObject) will make it easier to identify objects when we handle collision detection.

Here is the code for the GameObject class. Notice that it inherits from CCSprite and that we initialize the default type to kGameObjectNone (there is an enum defined in Constants.h, which is not shown here but will be included in the sample source).

// GameObject.h
#import "cocos2d.h"
#import "Constants.h"

@interface GameObject : CCSprite {
    GameObjectType  type;
}

@property (nonatomic, readwrite) GameObjectType type;
@end

////////////////////////////////////////////////////////

// GameObject.m
@implementation GameObject
@synthesize type;

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

- (void)dealloc
{
    [super dealloc];
}
@end

The next one shown is the Player class. Note that the extension for the Player class is Player.mm (two m’s). Do you see the Box2d’s b2Body object that is declared? Box2d is written in C++ not Objective-C, so this is the reason we must make the Player class support both Objective-C and C++ by naming the extension with two m’s. With only one, you will see a lot of errors during compilation. You can see that the type is set to kGameObjectPlayer. You will see the “type” come into play when we discuss collision detection later.

The Player class will be in charge of creating the Box2d physics object and adding itself into the physics world when the createBox2dObject method is called. We set the custom user data (playerBodyDef.userData) to “self” so that we can access the Cocos2d object from the Box2d’s contact listener. It also has methods to make the Player move to the right and jump, which will also be discussed later in the tutorial.

// Player.h
#import "cocos2d.h"
#import "Box2D.h"
#import "GameObject.h"

@interface Player : GameObject {
    b2Body          *body;
}

-(void) createBox2dObject:(b2World*)world;
-(void) jump;
-(void) moveRight;

@property (nonatomic, readwrite) b2Body *body;
@end

///////////////////////////////////////////////////////

// Player.mm
#import "Player.h"
#import "Constants.h"

@implementation Player
@synthesize body;

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

-(void) createBox2dObject:(b2World*)world {
    b2BodyDef playerBodyDef;
	playerBodyDef.type = b2_dynamicBody;
	playerBodyDef.position.Set(self.position.x/PTM_RATIO, self.position.y/PTM_RATIO);
	playerBodyDef.userData = self;
	playerBodyDef.fixedRotation = true;
	
	body = world->CreateBody(&playerBodyDef);
	
	b2CircleShape circleShape;
	circleShape.m_radius = 0.7;
	b2FixtureDef fixtureDef;
	fixtureDef.shape = &circleShape;
	fixtureDef.density = 1.0f;
	fixtureDef.friction = 1.0f;
	fixtureDef.restitution =  0.0f;
	body->CreateFixture(&fixtureDef);
}

-(void) moveRight {
    b2Vec2 impulse = b2Vec2(7.0f, 0.0f);
    body->ApplyLinearImpulse(impulse, body->GetWorldCenter());		
}

-(void) jump {
    b2Vec2 impulse = b2Vec2(4.0f, 15.0f);
    body->ApplyLinearImpulse(impulse, body->GetWorldCenter());		    
}
@end

As for the platform object, there isn’t a specific Platform object. For convenience, we simply allocate a GameObject object and specifically set the type to kGameObjectPlatform (see makeBox2dObjectAt method in GameScene way below).

User Input / Touch Detection

The following two lines inside GameScene’s init method are responsible for enabling the touch input. The swallowsTouches argument on line 3 says that whatever touches that GameScene receives is “swallowed” or ends in GameScene and is not further propagated.

// Inside GameScene's init method
self.isTouchEnabled = YES;
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];   

The method below is called when the mouse is clicked in the simulator or the screen is touched on the device. We get the location of the touch, and if the touch is on the left half of the screen, we move the player to the right. If the touch is on the right half of the screen, we make the player jump. We are not using it in this tutorial, but if you need it, then there is also a complimentary method to detect when the touch has ended. Note that this code is for a single touch, not multi-touches.

// GameScene.mm
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {    
    CGPoint location = [touch locationInView:[touch view]];
    location = [[CCDirector sharedDirector] convertToGL:location];
    if (location.x <= screenSize.width / 2) {
        [player moveRight];        
    } else {
        [player jump];
    }
    return TRUE;
}

-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
    CCLOG(@"Touch ended");
}

Applying Forces to Game Objects to Make it Move and Jump

To make the player move and jump, we apply a force to the player object. Recall that the Player class has the createBox2dObject method which should be called to add a physics object representing the player into the physics world. To move the player to the right, we simply tell Box2d to apply a positive 'x' force to the physics object that represents the player. And to make it jump, we apply a positive 'x' force along with a positive 'y' force to the physics object. In this case, we are applying an impulse force (it's a sudden burst of force) but you are not limited to it (please see the Box2d manual for information on other kind of forces). All the rest is handled by the Box2d physics engine. Once the force has been applied, we update the player's position in the main game loop with the new position provided to us by the physics engine (see the complete GameScene source further down, inside the update method).

-(void) moveRight {
    b2Vec2 impulse = b2Vec2(7.0f, 0.0f);
    body->ApplyLinearImpulse(impulse, body->GetWorldCenter());		
}

-(void) jump {
    b2Vec2 impulse = b2Vec2(4.0f, 15.0f);
    body->ApplyLinearImpulse(impulse, body->GetWorldCenter());		    
}

Collision Detection

For our simple example, only two objects exist in the world (player and platform). In order to detect when the player has made contact with the platform and when the player has lost contact with the platform, we must set up a contact listener and tell Box2d to use said contact listener. The contact listener is a C++ class which inherits from Box2d's b2ContactListener class.

The two functions that we are most interested in are BeginContact and EndContact. As the function name suggests, BeginContact is called whenever two objects in the world make contact with each other. And EndContact is called as soon as the contact is broken. You see that a b2Contact object is passed to the functions, which can be used to get the user data. The b2Contact object has two function calls GetFixtureA() and GetFixtureB() which can be used to return the two Box2d bodies involved in the collision, and subsequently the user data (our GameObject objects) stored in them. Recall that all the objects in our app is inherited from the GameObject class, so we can safely cast the user data for both to GameObjects. Once we have the two GameObjects, we can check the "type" property of each of the objects to check whether it is the player or the platform. It is important to remember that no guarantee is made as to which object of the two objects is the player object or the platform object. It is up to us to determine their types, so both objects must be checked to see if it is one or the other.

To help keep the code cleaner and easier to read, there are two macros called IS_PLAYER(x,y) and IS_PLATFORM(x,y) which test whether either one of the the objects is a player or a platform. For this simple example, a simple message is printed to the console when the contact is made or lost. In a real game, you could, for example, play a sound effect when two objects collide. Also, please remember that although it is tempting to implement game logic that alters the physics world inside a contact callback, this is not allowed. The Box2d manual states that...

...for example, you may have a collision that applies damage and try to destroy the associated actor and its rigid body. However, Box2D does not allow you to alter the physics world inside a callback because you might destroy objects that Box2D is currently processing, leading to orphaned pointers (http://www.box2d.org/manual.html)

// ContactListener.h
#import "Box2D.h"

class ContactListener : public b2ContactListener {
public:
	ContactListener();
	~ContactListener();
	
	virtual void BeginContact(b2Contact *contact);
	virtual void EndContact(b2Contact *contact);
	virtual void PreSolve(b2Contact *contact, const b2Manifold *oldManifold);
	virtual void PostSolve(b2Contact *contact, const b2ContactImpulse *impulse);
};

/////////////////////////////

// ContactListener.mm
#import "ContactListener.h"
#import "Constants.h"
#import "GameObject.h"

#define IS_PLAYER(x, y)         (x.type == kGameObjectPlayer || y.type == kGameObjectPlayer)
#define IS_PLATFORM(x, y)       (x.type == kGameObjectPlatform || y.type == kGameObjectPlatform)


ContactListener::ContactListener() {
}

ContactListener::~ContactListener() {
}

void ContactListener::BeginContact(b2Contact *contact) {
	GameObject *o1 = (GameObject*)contact->GetFixtureA()->GetBody()->GetUserData();
	GameObject *o2 = (GameObject*)contact->GetFixtureB()->GetBody()->GetUserData();
	
	if (IS_PLATFORM(o1, o2) && IS_PLAYER(o1, o2)) {
        CCLOG(@"-----> Player made contact with platform!");
    }
}

void ContactListener::EndContact(b2Contact *contact) {
	GameObject *o1 = (GameObject*)contact->GetFixtureA()->GetBody()->GetUserData();
	GameObject *o2 = (GameObject*)contact->GetFixtureB()->GetBody()->GetUserData();
    
	if (IS_PLATFORM(o1, o2) && IS_PLAYER(o1, o2)) {
        CCLOG(@"-----> Player lost contact with platform!");
    }
}

void ContactListener::PreSolve(b2Contact *contact, const b2Manifold *oldManifold) {
}

void ContactListener::PostSolve(b2Contact *contact, const b2ContactImpulse *impulse) {
}

You then register the contact listener when you set up the physics world.

 contactListener = new ContactListener();
 world->SetContactListener(contactListener);

Wrap-up and Loose Ends

Below is the code for the GameScene class. If certain things look unfamiliar to you, please refer to the previous two tutorials that I mentioned in the beginning of the article. If you have read those tutorials, then things should be familiar except for the contact listener registration code and the touch detection code, which I have already explained earlier in the tutorial.

You can find the complete source code for download and the demo video at the end of the tutorial.

// GameScene.h

#import "cocos2d.h"
#import "Box2D.h"
#import "GLES-Render.h"
#import "ContactListener.h"

@class Player;

@interface GameScene : CCLayer
{
    CGSize screenSize;
	b2World* world;
	GLESDebugDraw *m_debugDraw;
	CCTMXTiledMap *tileMapNode;	
    Player *player;
    ContactListener *contactListener;
}

+(id) scene;
@end

///////////////////////////////////////////////

// GameScene.mm
#import "GameScene.h"
#import "Constants.h"
#import "Player.h"
#import "GameObject.h"

@interface GameScene(Private) 
-(void) setupPhysicsWorld;
@end

@implementation GameScene

+(id) scene
{
	// 'scene' is an autorelease object.
	CCScene *scene = [CCScene node];
	
	// 'layer' is an autorelease object.
	GameScene *layer = [GameScene node];
	
	// add layer as a child to scene
	[scene addChild: layer];
	
	// return the scene
	return scene;
}

- (void) makeBox2dObjAt:(CGPoint)p 
			   withSize:(CGPoint)size 
				dynamic:(BOOL)d 
			   rotation:(long)r 
			   friction:(long)f 
				density:(long)dens 
			restitution:(long)rest 
				  boxId:(int)boxId {
	
	// Define the dynamic body.
	//Set up a 1m squared box in the physics world
	b2BodyDef bodyDef;
//	bodyDef.angle = r;
	
	if(d)
		bodyDef.type = b2_dynamicBody;
	
	bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO);
    
    GameObject *platform = [[GameObject alloc] init];
    [platform setType:kGameObjectPlatform];
	bodyDef.userData = platform;
	
	b2Body *body = world->CreateBody(&bodyDef);
	
	// Define another box shape for our dynamic body.
	b2PolygonShape dynamicBox;
	dynamicBox.SetAsBox(size.x/2/PTM_RATIO, size.y/2/PTM_RATIO);
	
	// Define the dynamic body fixture.
	b2FixtureDef fixtureDef;
	fixtureDef.shape = &dynamicBox;	
	fixtureDef.density = dens;
	fixtureDef.friction = f;
	fixtureDef.restitution = rest;
	body->CreateFixture(&fixtureDef);
}



- (void) drawCollisionTiles {
	CCTMXObjectGroup *objects = [tileMapNode objectGroupNamed:@"Collision"];
	NSMutableDictionary * objPoint;
	
	int x, y, w, h;	
	for (objPoint in [objects objects]) {
		x = [[objPoint valueForKey:@"x"] intValue];
		y = [[objPoint valueForKey:@"y"] intValue];
		w = [[objPoint valueForKey:@"width"] intValue];
		h = [[objPoint valueForKey:@"height"] intValue];	
		
		CGPoint _point=ccp(x+w/2,y+h/2);
		CGPoint _size=ccp(w,h);
		
		[self makeBox2dObjAt:_point 
					withSize:_size 
					 dynamic:false 
					rotation:0 
					friction:1.5f 
					 density:0.0f 
				 restitution:0 
					   boxId:-1];
	}
}

- (void) addScrollingBackgroundWithTileMap {
	tileMapNode = [CCTMXTiledMap tiledMapWithTMXFile:@"scroller.tmx"];
	tileMapNode.anchorPoint = ccp(0, 0);
	[self addChild:tileMapNode];
}


// initialize your instance here
-(id) init
{
	if( (self=[super init])) {
		
		// enable touches
		self.isTouchEnabled = YES;
				
		screenSize = [CCDirector sharedDirector].winSize;
		[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];   
		
		[self setupPhysicsWorld];
		
		[self addScrollingBackgroundWithTileMap];
		[self drawCollisionTiles];
		
		player = [Player spriteWithFile:@"Icon-Small.png"];        
		player.position = ccp(100.0f, 180.0f);
		[player createBox2dObject:world];
		
		[self addChild:player];
		
        // Start main game loop
		[self scheduleUpdate];
	}
	return self;
}

-(void) setupPhysicsWorld {
    b2Vec2 gravity = b2Vec2(0.0f, -9.8f);
    bool doSleep = true;
    world = new b2World(gravity, doSleep);
    
    m_debugDraw = new GLESDebugDraw(PTM_RATIO);
    world->SetDebugDraw(m_debugDraw);
    uint32 flags = 0;
    flags += b2DebugDraw::e_shapeBit;
    m_debugDraw->SetFlags(flags);

    contactListener = new ContactListener();
    world->SetContactListener(contactListener);
}

-(void) draw {
	glDisable(GL_TEXTURE_2D);
	glDisableClientState(GL_COLOR_ARRAY);
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	
	world->DrawDebugData();
	
	// restore default GL states
	glEnable(GL_TEXTURE_2D);
	glEnableClientState(GL_COLOR_ARRAY);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
}

- (void) update:(ccTime)dt {
	//It is recommended that a fixed time step is used with Box2D for stability
	//of the simulation, however, we are using a variable time step here.
	//You need to make an informed choice, the following URL is useful
	//http://gafferongames.com/game-physics/fix-your-timestep/
	
	int32 velocityIterations = 8;
	int32 positionIterations = 1;
	
	// Instruct the world to perform a single step of simulation. It is
	// generally best to keep the time step and iterations fixed.
	world->Step(dt, velocityIterations, positionIterations);
	
	
	//Iterate over the bodies in the physics world
	for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()) {
		if (b->GetUserData() != NULL) {
			//Synchronize the AtlasSprites position and rotation with the corresponding body
			CCSprite *myActor = (CCSprite*)b->GetUserData();
			myActor.position = CGPointMake( b->GetPosition().x * PTM_RATIO, 
							b->GetPosition().y * PTM_RATIO);
			myActor.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle());
		}	
	}	
	
	b2Vec2 pos = [player body]->GetPosition();
	CGPoint newPos = ccp(-1 * pos.x * PTM_RATIO + 50, self.position.y * PTM_RATIO);	
	[self setPosition:newPos];
}

-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {    
    CGPoint location = [touch locationInView:[touch view]];
    location = [[CCDirector sharedDirector] convertToGL:location];
    if (location.x <= screenSize.width / 2) {
        [player moveRight];        
    } else {
        [player jump];
    }
	return TRUE;
}

// on "dealloc" you need to release all your retained objects
- (void) dealloc {
    // in case you have something to dealloc, do it in this method

    delete contactListener;
    delete world;
    world = NULL;
	
    delete m_debugDraw;
	
    // don't forget to call "super dealloc"
    [super dealloc];
}
@end

Demo video of the Simple Platformer in action

Download source: SimplePlatformer.zip

Happy Saint Patrick's Day! I wrote this tutorial while knocking back some Guinness, so I must blame alcohol if you see any glaring mistakes 🙂

Mar 112011
 

Life has once again conspired to keep me away from updating the blog as frequently as I promised. I meant to write a simple platformer tutorial, but I had an interview with Google that seriously threw a monkey wrench into my plans. So I am just catching up on the backlog of tasks I need to complete for the game that I am working on, which is shaping up nicely! Hopefully, I will be able to put together a simple tutorial in the near future.

In the meanwhile, here is a very simple change you can make to your game that will instantly increase your frame rate from 30 FPS to 60 FPS especially on the first generation (2G) or the second generation (3G) iPhones. Best of all, the change won’t take you more than a few seconds to make! Inside GameConfig.h, you will see GAME_AUTOROTATION defined in one of two ways:

The below uses UIViewController and Core Animation to perform the auto rotation:

#define GAME_AUTOROTATION kGameAutorotationUIViewController

Or you may see below, which will mean Cocos2d’s CCDirector will handle the auto rotation:

#define GAME_AUTOROTATION kGameAutorotationCCDirector

It is much faster if you let Cocos2d handle the auto rotation because rotation is done by a cheap call to glRotatef() function. This instantly increased my frame rate from 30 FPS to 60 FPS on the 1st generation iPhone.

If you would like more details on why this is, please take a look at the Cocos2d wiki at http://www.cocos2d-iphone.org/wiki/doku.php/prog_guide:autorotation