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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Shaping the Hills in Box2D

Right now you have a Box2D shape representing the ground along the bottom of the screen, but what you really want is a shape representing the hills.

Luckily, this is quite easy since you already have all of the pieces in place!

  • You have an array of vertices of the top of the hill (borderVertices). You created this in the last tutorial when you generated the triangle strip in resetHillVertices.
  • You have a method that is called whenever the vertices change (resetBox2DBody).

So you 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);
    
    b2EdgeShape 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.Set(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!

Box2D hills with slopes made via edges

Adding the Seal

All this time you’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 Hero for Class, CCSprite for Subclass, and click Next, then Create.

Then rename Hero.m to Hero.mm (remember you need to use the .mm extension since this file will use Box2D).

Replace Hero.h with the following:

#import "Box2D.h"

@interface Hero : CCSprite

- (id)initWithWorld:(b2World *)world;
- (void)update;

@end

This is pretty simple stuff – just imports Box2D.h and declares methods.

Then switch to Hero.mm and replace it with the following:

#import "Hero.h"
#import "HelloWorldLayer.h"

@interface Hero() {
    b2World *_world;
    b2Body *_body;
    BOOL _awake;
}
@end

@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 / CC_CONTENT_SCALE_FACTOR();
    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 you’re going to add a sprite batch node inside Terrain.m.

Start by making the following modifications to Terrain.h:

// Inside @interface after the variable declaration block
@property (retain) CCSpriteBatchNode * batchNode;

Then switch to the Terrain.mm file and make the following modifications:

// Add at bottom of initWithWorld:
_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.mm:

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

// Add inside @interface
Hero * _hero;

// Add to bottom of onEnter
_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!

Seal visible, but partially offscreen

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 eigth 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!

Offset modified so seal is onscreen

Note: If you’re running this on a retina display, the seal will be half size. This is because we haven’t provided retina-sized artwork – in a real game, you’d obviously provide that :]

Making the Seal Move

You’re getting close to a game at this point – you have a seal, you just need to let him fly!

The strategy you’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 hill, 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 @interface block
@property (readonly) BOOL awake;
- (void)wake;
- (void)dive;
- (void)limitVelocity;

Then make the following modifications to Hero.mm:


// 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 seal 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.mm by adding the following instance variable inside the @interface:

BOOL _tapDown;

Then make the following changes to the methods in HellowWorldLayer.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!

Your seal can now fly high into the air!

Ali Hafizji

Contributors

Ali Hafizji

Author

Over 300 content creators. Join our team.