How To Create A Game Like Tiny Wings with Cocos2D 2.X Part 2

In this second part of this Tiny Wings tutorial series, you’ll learn how to add the main character to the game, and use Box2D to simulate his movement. By Ali Hafizji.

Leave a rating/review
Save for later
Share

Update 5/19/2013 Fully updated for Cocos2D 2.X. (original post by Ray Wenderlich, update by Ali Hafizji).

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, I first covered how to create dynamic background textures for the game.

In the first part of the tutorial series, I covered how to create the dynamic hills that you need in the game.

In this second and final part of the series, you’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 you left it off in the previous tutorial.

Next, you’re going to add what I like to think of as the “Hello, Box2D!” code. You’ll create a Box2D world and add some code to set up debug drawing and add some test shapes, just to make sure you have everything working.

Start by making the following modifications to HelloWorldLayer.mm:

// Add to top of file
#import "Box2D.h"

// Add inside @interface
b2World * _world;

Also add the following constant to the Prefix.pch file:

#define PTM_RATIO   32.0

This adds the Box2D header and the debug draw header file. It also declares the pixel-to-meter ratio (PTM_RATIO) to 32.0. As a refresher, this is what you need to use to convert between Box2D units (meters) and Cocos2D units (points).

Then add the following new methods to HelloWorldLayer.mm, above the onEnter method:

- (void)setupWorld {
    b2Vec2 gravity = b2Vec2(0.0f, -7.0f);
    bool doSleep = true;
    _world = new b2World(gravity);
    _world->SetAllowSleeping(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 creates 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. You’ll use this to create a test object wherever you tap for testing purposes.

You’re not done with HelloWorldLayer.mm yet – make a few more modifications to it now:

// Add to the TOP of onEnter
[self setupWorld];

// Replace line to create Terrain in onEnter 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 onEnter just calls the new method you wrote earlier to set up the Box2D world. You also modified 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. You’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:withEvent: method 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 you want to know the position within the terrain, not the position on the screen.

Next, you’re going to make some changes to Terrain.h/m. Make the following changes to Terrain.h:

// Add to top of file
#import "Box2D.h"

// Add after setOffsetX: method
- (id)initWithWorld:(b2World *)world;

This just includes Box2D, and predeclares a new initializer that will take the Box2D world as a parameter.

Next open Terrain.m and make the following changes:

//Add to top of file
#import "GLES-Render.h"

// Add inside @interface
b2World *_world;
b2Body *_body;
GLESDebugDraw * _debugDraw;

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);
    
    b2EdgeShape shape;
    
    float minY = 0;
    CGSize winSize = [CCDirector sharedDirector].winSize;
    if (winSize.height > 480) {
        minY = (1136 - 1024)/4;
    }
    
    b2Vec2 ep1 = b2Vec2(p0.x/PTM_RATIO, minY/PTM_RATIO);
    b2Vec2 ep2 = b2Vec2(p1.x/PTM_RATIO, minY/PTM_RATIO);    
    shape.Set(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, you’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);
    _world->SetDebugDraw(_debugDraw);
    _debugDraw->SetFlags(GLESDebugDraw::e_shapeBit | GLESDebugDraw::e_jointBit);
}

// Replace init with the following
- (id)initWithWorld:(b2World *)world {
    if ((self = [super init])) {
        _world = world;
        [self setupDebugDraw];
        [self generateHills];
        [self resetHillVertices];

        self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];
    }
    return self;
}

// Add at bottom of draw
_world->DrawDebugData();

Every time the hill vertices are reset, you 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 you’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 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!
Hills with ground and Box2D objects

Ali Hafizji

Contributors

Ali Hafizji

Author

Over 300 content creators. Join our team.