This is the second part of a tutorial series that shows you how to make a game like the popular Tiny Wings game by Andreas Illiger!
In the prerequisite for this tutorial series, we first covered how to create dynamic hill and background textures for the game.
In the first part of the tutorial series, we covered how to create the dynamic hills that you need in the game.
In this second and final part of the series, we’ll finally get to the fun stuff – how to add the main character to the game, and use Box2D to simulate his movement!
As a reminder, this tutorial is based on a great demo project written by Sergey Tikhonov – so special thanks again to Sergey!
This tutorial assumes you are familiar with Cocos2D and Box2D. If you are new to Cocos2D or Box2D, you should check out some of the other Cocos2D and Box2D tutorials on this site first.
Getting Started
If you don’t have it already, download a copy of the sample project where we left it off in the previous tutorial.
Next, we’re going to add what I like to think of as the “Hello, Box2D!” code. We’ll create a Box2D world and add some code to set up debug drawing and add some test shapes, just to make sure we have everything working so far.
Start by making the following modifications to HelloWorldLayer.h:
// Add to top of file #import "Box2D.h" #define PTM_RATIO 32.0 // Add inside @interface b2World * _world; |
This includes the Box2D header file and the debug draw header file, and predeclares the variable to keep track of the Box2D world and debug draw class.
It also declares the pixel-to-meter ratio (PTM_RATIO) to 32.0. As a refresher, this is what we need to use to convert between Box2D units (meters) and Cocos2D units (points).
Then add the following new methods to HelloWorldLayer.mm, above the init method:
- (void)setupWorld { b2Vec2 gravity = b2Vec2(0.0f, -7.0f); bool doSleep = true; _world = new b2World(gravity, doSleep); } - (void)createTestBodyAtPostition:(CGPoint)position { b2BodyDef testBodyDef; testBodyDef.type = b2_dynamicBody; testBodyDef.position.Set(position.x/PTM_RATIO, position.y/PTM_RATIO); b2Body * testBody = _world->CreateBody(&testBodyDef); b2CircleShape testBodyShape; b2FixtureDef testFixtureDef; testBodyShape.m_radius = 25.0/PTM_RATIO; testFixtureDef.shape = &testBodyShape; testFixtureDef.density = 1.0; testFixtureDef.friction = 0.2; testFixtureDef.restitution = 0.5; testBody->CreateFixture(&testFixtureDef); } |
If you’re familiar with Box2D, these methods should be review.
The setupWorld method reates a Box2D world with a certain amount of gravity – a little less than “Earth standard” of -9.8 m/s².
The createTestBodyAtPostition method creates a test object – a circle with a 25 point radius. We’ll use this to create a test object wherever you tap for testing purposes, and we’ll remove this later on.
You’re not done with HelloWorldLayer.mm yet – make a few more modifications to it now:
// Add to the TOP of init [self setupWorld]; // Replace line to create Terrain in init with the following _terrain = [[[Terrain alloc] initWithWorld:_world] autorelease]; // Add to the TOP of update static double UPDATE_INTERVAL = 1.0f/60.0f; static double MAX_CYCLES_PER_FRAME = 5; static double timeAccumulator = 0; timeAccumulator += dt; if (timeAccumulator > (MAX_CYCLES_PER_FRAME * UPDATE_INTERVAL)) { timeAccumulator = UPDATE_INTERVAL; } int32 velocityIterations = 3; int32 positionIterations = 2; while (timeAccumulator >= UPDATE_INTERVAL) { timeAccumulator -= UPDATE_INTERVAL; _world->Step(UPDATE_INTERVAL, velocityIterations, positionIterations); _world->ClearForces(); } // Add to bottom of ccTouchesBegan UITouch *anyTouch = [touches anyObject]; CGPoint touchLocation = [_terrain convertTouchToNodeSpace:anyTouch]; [self createTestBodyAtPostition:touchLocation]; |
The code you added to init just calls the new method you wrote earlier to set up the Box2D world. We also modify the line to initialize the Terrain class to pass in the Box2D world. This way it can use the world to create a Box2D body for the hill eventually. We’ll write some placeholder code for this next.
The code you added to update gives Box2D time to run its physics simulation each update by calling _world->Step. Note that this is a fixed-timestep implementation, which is better than variable rate timesteps when running physics simulations. For more information on how this works, check out our Cocos2D book‘s Box2D chapters.
The code you added to ccTouchesBegan runs the helper method to create a Box2D body wherever you tapped. Again, this is just for debugging to verify Box2D is working OK so far.
Notice that this gets the coordinate of the touch within the terrain coordinates. This is because the terrain will be scrolling, and we want to know the position within the terrain, not the position on the screen.
Next, let’s make some changes to Terrain.h/m. Make the following changes to Terrain.h:
// Add to top of file #import "Box2D.h" #import "GLES-Render.h" // Add inside @interface b2World *_world; b2Body *_body; GLESDebugDraw * _debugDraw; // Add after @interface - (id)initWithWorld:(b2World *)world; |
This just includes Box2D, creates some instance variables to keep track of the Box2D world and the Box2D body we’ll be creating for the hills, and predeclares our new initializer that will take the Box2D world as a parameter.
Then add the following new method to Terrain.m, above generateHills:
- (void) resetBox2DBody { if(_body) return; CGPoint p0 = _hillKeyPoints[0]; CGPoint p1 = _hillKeyPoints[kMaxHillKeyPoints-1]; b2BodyDef bd; bd.position.Set(0, 0); _body = _world->CreateBody(&bd); b2PolygonShape shape; b2Vec2 ep1 = b2Vec2(p0.x/PTM_RATIO, 0); b2Vec2 ep2 = b2Vec2(p1.x/PTM_RATIO, 0); shape.SetAsEdge(ep1, ep2); _body->CreateFixture(&shape, 0); } |
This is just a helper method that creates a Box2D body along the bottom of the hill, to represent the “ground.” This is just a temporary method so we can add bodies and have them hit something rather than falling endlessly, we’ll replace this later to model the hill itself.
For now, all it does is figure out the x-coordinate for the first and last keypoints, and draw an edge connecting the two.
Next, add a few more modifications to Terrain.m in order to call this code, and set up debug drawing:
// Add inside resetHillVertices, right after "prevToKeyPointI = _toKeyPointI" line: [self resetBox2DBody]; // Add new method above init - (void)setupDebugDraw { _debugDraw = new GLESDebugDraw(PTM_RATIO*[[CCDirector sharedDirector] contentScaleFactor]); _world->SetDebugDraw(_debugDraw); _debugDraw->SetFlags(b2DebugDraw::e_shapeBit | b2DebugDraw::e_jointBit); } // Replace init with the following - (id)initWithWorld:(b2World *)world { if ((self = [super init])) { _world = world; [self setupDebugDraw]; [self generateHills]; [self resetHillVertices]; } return self; } // Add at bottom of draw glDisable(GL_TEXTURE_2D); glDisableClientState(GL_COLOR_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); _world->DrawDebugData(); glEnable(GL_TEXTURE_2D); glEnableClientState(GL_COLOR_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); |
Every time the hill vertices are reset, we call resetBox2DBody to create the Box2D representation of the visible hills. Right now the body never changes (it’s just adding a line for the ground) but later on we’ll fix up the Box2D body to model the visible hills.
setupDebugDraw and the draw code is what you need to set up debug drawing for the Box2D objects. If you’re familiar with Box2D, this should be review.
However, you may be wondering why the debug draw code is in Terrain.m, instead of in HelloWorldLayer.mm. This is because scrolling is implemented in this game by moving Terrain.m. So to match up the coordinate system of Box2D with what’s visible on the screen, we need to add the debug drawing code into Terrain.m.
One last step. If you try to compile right now, you’ll get a ton of errors. This is because Terrain.m imports Terrain.h, which imports HelloWorldLayer.h, which imports Box2D.h. And whenever you have a .m file import C++ code (like Box2D) you get a ton of errors.
Luckily the solution is simple – renamed Terrain.m to Terrain.mm.
Compile and run, and now as you tap you can see a lot of circle objects fall into your scene!

Shaping the Hills in Box2D
Right now we have a Box2D shape representing the ground along the bottom of the screen, but what we really want is a shape representing the hills.
Luckily, this is quite easy since we already have all of the pieces in place!
- We have an array of the vertices of the top of the hill (borderVertices). We created this in the last tutorial when we generated the triangle strip in resetHillVertices.
- We have a method that is called whenever the vertices change (resetBox2DBody).
So we just need to replace resetBox2DBody to create edges for each entry in borderVertices! Replace it with the following code:
- (void) resetBox2DBody { if(_body) { _world->DestroyBody(_body); } b2BodyDef bd; bd.position.Set(0, 0); _body = _world->CreateBody(&bd); b2PolygonShape shape; b2Vec2 p1, p2; for (int i=0; i<_nBorderVertices-1; i++) { p1 = b2Vec2(_borderVertices[i].x/PTM_RATIO,_borderVertices[i].y/PTM_RATIO); p2 = b2Vec2(_borderVertices[i+1].x/PTM_RATIO,_borderVertices[i+1].y/PTM_RATIO); shape.SetAsEdge(p1, p2); _body->CreateFixture(&shape, 0); } } |
This new implementation first looks to see if there’s already a Box2D body, and destroys the old one first before continuing.
Then it creates a new body, and starts looping through the array of border vertices, which contain the vertices for the top of the hill. For each piece, it creates an edge connecting the two.
Pretty easy, eh? Compile and run, and now you’ll have a nice Box2D body following the slope of your hills!
Adding the Seal
All this time we’ve been working with a project named Tiny Seal, but there’s no seal!
That is just a tragedy, so let’s address that right away.
First, download and unzip the resources file for this project, and drag the “Sprite Sheets” and “Sounds” directories into your Xcode project. For each directory, verify “Copy items into destination group’s folder” is selected, and click Finish.
Go to File\New\New File, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter CCSprite for Subclass of and click Next, name the file Hero.mm (note the .mm extension since this file will use Box2D), and click Finish.
Replace Hero.h with the following:
#import "cocos2d.h" #import "Box2D.h" #define PTM_RATIO 32.0 @interface Hero : CCSprite { b2World *_world; b2Body *_body; BOOL _awake; } - (id)initWithWorld:(b2World *)world; - (void)update; @end |
This is pretty simple stuff – just imports Box2D.h and predeclares a variable to keep track of the world and the Box2D body for the seal.
Then switch to Hero.mm and replace it with the following:
#import "Hero.h" @implementation Hero - (void)createBody { float radius = 16.0f; CGSize size = [[CCDirector sharedDirector] winSize]; int screenH = size.height; CGPoint startPosition = ccp(0, screenH/2+radius); b2BodyDef bd; bd.type = b2_dynamicBody; bd.linearDamping = 0.1f; bd.fixedRotation = true; bd.position.Set(startPosition.x/PTM_RATIO, startPosition.y/PTM_RATIO); _body = _world->CreateBody(&bd); b2CircleShape shape; shape.m_radius = radius/PTM_RATIO; b2FixtureDef fd; fd.shape = &shape; fd.density = 1.0f; fd.restitution = 0.0f; fd.friction = 0.2; _body->CreateFixture(&fd); } - (id)initWithWorld:(b2World *)world { if ((self = [super initWithSpriteFrameName:@"seal1.png"])) { _world = world; [self createBody]; } return self; } - (void)update { self.position = ccp(_body->GetPosition().x*PTM_RATIO, _body->GetPosition().y*PTM_RATIO); b2Vec2 vel = _body->GetLinearVelocity(); b2Vec2 weightedVel = vel; float angle = ccpToAngle(ccp(vel.x, vel.y)); if (_awake) { self.rotation = -1 * CC_RADIANS_TO_DEGREES(angle); } } @end |
The createBody method creates a circle shape representing the seal. This is almost exactly the same as the createTestBodyAtPosition method you wrote earlier, except it bases the radius on the seal size (actually smaller than the seal, for the collisions to work right).
Also, the friction is set to 0.2 (so the seal is extremely slippery) and the restitution is set to 0 (so the seal doesn’t bounce when he hits the ground).
It also sets some linear damping on the body so it tends to slow down over time, and sets the body to fixed rotation since it doesn’t really need to rotate for this game.
The initWithWorld method initializes the sprite to a particular sprite frame (seal1.png), squirrels away a copy of the world, and calls the above createBody method.
The update method updates the position of the seal sprite to match the position of the Box2D body, and the rotation of the sprite based on the seal’s velocity.
Next, you need to make a few modifications to Terrain.h and Terrain.mm, because we’re going to add a sprite batch node inside Terrain.m.
Start by making the following modifications to Terrain.h:
// Inside @interface CCSpriteBatchNode * _batchNode; // After @implementation @property (retain) CCSpriteBatchNode * batchNode; |
And then the following modifications to Terrain.mm:
// Add to top of file @synthesize batchNode = _batchNode; // Add at bottom of init _batchNode = [CCSpriteBatchNode batchNodeWithFile:@"TinySeal.png"]; [self addChild:_batchNode]; [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"TinySeal.plist"]; |
This simply creates a batch node for the TinySeal.png sprite sheet, and loads the sprite frame definitions from TinySeal.plist into the sprite frame cache.
Almost done! Just make the following modifications to HelloWorldLayer.h:
// Add to top of file #import "Hero.h" // Add inside @interface Hero * _hero; |
And the following to HelloWorldLayer.mm:
// Add to bottom of init _hero = [[[Hero alloc] initWithWorld:_world] autorelease]; [_terrain.batchNode addChild:_hero]; // In update, comment out the three lines starting with PIXELS_PER_SECOND and add the following [_hero update]; float offset = _hero.position.x; |
Compile and run, and you should now see a happy seal on the left side of the screen!
But it’s kind of annoying he’s offscreen. It would be better if we showed him more to the right a bit.
Luckily this is easy to fix! Just open up Terrain.mm and replace setOffsetX with the following:
- (void) setOffsetX:(float)newOffsetX { CGSize winSize = [CCDirector sharedDirector].winSize; _offsetX = newOffsetX; self.position = CGPointMake(winSize.width/8-_offsetX*self.scale, 0); [self resetHillVertices]; } |
This adds one eighth of the winSize.width to the passed-in offset, so the seal appears to the right a bit. Compile and run, and now the seal is more visible!
Making the Seal Move
We’re getting close to a game at this point – we have a seal, we just need to let him fly!
The strategy we’re going to take is the following:
- The first time the screen is tapped, make the seal jump up to the right a bit to get him started.
- Whenever a touch is held down, apply a force to the seal to push him down. This will make him go quickly down hills, giving him velocity that will make him fly up when he goes up the next hill.
- Add some code to make sure the seal moves at least a set amount. We don’t want the poor seal to get stuck!
Let’s try this out. Make the following modifications to Hero.h:
// Add after @implementation @property (readonly) BOOL awake; - (void)wake; - (void)dive; - (void)limitVelocity; |
Then make the following modifications to Hero.mm:
// Add to top of file @synthesize awake = _awake; // Add new methods - (void) wake { _awake = YES; _body->SetActive(true); _body->ApplyLinearImpulse(b2Vec2(1,2), _body->GetPosition()); } - (void) dive { _body->ApplyForce(b2Vec2(5,-50),_body->GetPosition()); } - (void) limitVelocity { if (!_awake) return; const float minVelocityX = 5; const float minVelocityY = -40; b2Vec2 vel = _body->GetLinearVelocity(); if (vel.x < minVelocityX) { vel.x = minVelocityX; } if (vel.y < minVelocityY) { vel.y = minVelocityY; } _body->SetLinearVelocity(vel); } |
The wake method applies an impulse to the upper right to get the seal initially moving.
The dive method applies a strong impulse down, and an impulse slightly to the right. The down impulse will cause the seal to collide off of the hill, and the stronger the slope of the hill the more the bird will want to “fly up” once the next hill arrives.
limitVelocity just makes sure the seal is moving at least 5m/s² along the x-axis, and no less than -40m/s² along the y-axis.
Almost done – just a few modifications to HelloWorldLayer. Start with HelloWorldLayer.h by adding the following instance variable inside the @interface:
BOOL _tapDown; |
And the following changes to HelloWorldLayer.mm:
// Add at the top of the update method if (_tapDown) { if (!_hero.awake) { [_hero wake]; _tapDown = NO; } else { [_hero dive]; } } [_hero limitVelocity]; // Replace ccTouchesBegan with the following - (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self genBackground]; _tapDown = YES; } // Add new methods -(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { _tapDown = NO; } - (void)ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { _tapDown = NO; } |
Compile and run, and now you should have a seal that can fly through the air!
Fixing a Shaky Seal
You may notice when trying out the level that the seal switches rotation in a shaky manner as he slides along the ground.
One way to fix this is to take the weighted average of the past few linear velocities instead of always setting the position on the current velocity.
Let’s try this out. Add the following to Hero.h:
// Add to top of file #define NUM_PREV_VELS 5 // Add inside @interface b2Vec2 _prevVels[NUM_PREV_VELS]; int _nextVel; |
And modify the update method in Hero.mm as follows:
- (void)update { self.position = ccp(_body->GetPosition().x*PTM_RATIO, _body->GetPosition().y*PTM_RATIO); b2Vec2 vel = _body->GetLinearVelocity(); b2Vec2 weightedVel = vel; for(int i = 0; i < NUM_PREV_VELS; ++i) { weightedVel += _prevVels[i]; } weightedVel = b2Vec2(weightedVel.x/NUM_PREV_VELS, weightedVel.y/NUM_PREV_VELS); _prevVels[_nextVel++] = vel; if (_nextVel >= NUM_PREV_VELS) _nextVel = 0; float angle = ccpToAngle(ccp(weightedVel.x, weightedVel.y)); if (_awake) { self.rotation = -1 * CC_RADIANS_TO_DEGREES(angle); } } |
This takes the average of the past 5 velocities instead of always taking just the linear velocity. Compile and run, and now you should see a much smoother seal!
Zooming Out
One cool effect that Tiny Wings does is it zooms out the level the higher you go. This not only keeps the hero in view, but it adds to a cool feeling that you’re really moving!
To add this, simply add the following lines after the call to [_hero update] in HelloWorldLayer.mm’s update method:
CGSize winSize = [CCDirector sharedDirector].winSize; float scale = (winSize.height*3/4) / _hero.position.y; if (scale > 1) scale = 1; _terrain.scale = scale; |
If the hero is at winSize.height*3/4, the scale will be 1. If he is < winSize.height*3/4, the scale will increase. If he is > winSize.height*3/4, the scale will decrease, effectively zooming out.
Compile and run, and see how high you can fly!
Gratuitous Animation and Music
You know I couldn’t leave you guys hanging without some gratuitous animation and music!
Will only take a second for a bit more awesomeness! Start by making the following changes to Hero.h:
// Add inside @interface CCAnimation *_normalAnim; CCAnimate *_normalAnimate; // Add after @interface - (void)nodive; - (void)runForceAnimation; - (void)runNormalAnimation; |
This just declares the animation we’ll be creating, and a method we’ll call whenever the seal is NOT diving.
Next make the following modifications to Hero.mm:
// Add new methods - (void)runNormalAnimation { if (_normalAnimate || !_awake) return; _normalAnimate = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:_normalAnim]]; [self runAction:_normalAnimate]; } - (void)runForceAnimation { [_normalAnimate stop]; _normalAnimate = nil; [self setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"seal_downhill.png"]]; } - (void)nodive { [self runNormalAnimation]; } // Add at bottom of initWithWorld: _normalAnim = [[CCAnimation alloc] init]; [_normalAnim addFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"seal1.png"]]; [_normalAnim addFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"seal2.png"]]; _normalAnim.delay = 0.1; |
This creates an animation for when the seal is moving normally, and a method to run that animation. The “animation” for diving is actually just a single frame, so we add a helper method to set that too.
Finally make a few changes to HelloWorldLayer.mm:
// At top of file #import "SimpleAudioEngine.h" // At end of init [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"TinySeal.caf" loop:YES]; // At start of update, add an else case for the if (_tapDown): else { [_hero nodive]; } // Inside ccTouchesBegan [_hero runForceAnimation]; // Inside ccTouchesEnded AND ccTouchesCancelled [_hero runNormalAnimation]; |
Finally go to Terrain.mm and comment out the call to _world->DrawDebugData in the draw method.
Compile and run the code, and now your seal can ride the hills in style!
Where To Go From Here?
Here is the sample code from the above tutorial.
At this point, you have a basic game working. Why not try to tweak the behavior of the seal so he moves in a more natural way up and down the hills? Or start adding in items to pick up, decorations on the hills, points to collect – your imagination is the only limit!
If you extend the game to add any cool new features or have any comments or questions, please join the forum discussion below!
iPhoneCategory:
English













