21 June 2011

How To Create A Game Like Tiny Wings Part 1

Learn how to create a game like Tiny Wings!

Learn how to create a game like Tiny Wings!

Tiny Wings is an extremely popular game by Andreas Illiger involving a bird who tries to fly by catching a ride on hills.

At first glance, the gameplay of Tiny Wings looks very simple, but there are a lot of tricks going on under the hood. The hills and their textures are dynamically generated, and the game uses Box2D physics to simulate the movement of the bird.

Due to the popularity of the game and the cool technical tricks within, a lot of developers have been curious about how things are implemented.

Including you guys! You guys said you wanted this tutorial in the sidebar vote a few weeks back – well you want it, you got it! :]

This tutorial series is based on an excellent demo project written by Sergey Tikhonov that demonstrates how to implement some of the trickiest features of Tiny Wings. Thanks Sergey!

This tutorial series is split into three parts:

  1. Prerequisite: First review the How To Create Dynamic Textures with CCRenderTexture tutorial, which shows you how to create the hill and background textures you’ll be using in this tutorial.
  2. Part 1: You are here! This part will show you how to create the dynamic hills that you’ll need for a game like Tiny Wings.
  3. Part 2: The second part will show you how to add the Box2D gameplay that you’ll need for a game like Tiny Wings.

This tutorial assumes you are familiar with Cocos2D. If you are new to Cocos2D, you should check out some of the other Cocos2D tutorials on this site first.

Getting Started

If you don’t have it already, download the sample project where we left it off in the previous tutorial.

Next, create a class for the terrain by going to File\New\New File, choosing iOS\Cocoa Touch\Objective-C class, and clicking Next. Make the class a subclass of CCNode, click Next, name the class Terrain.m, and click Save.

Then open up Terrain.h and replace its contents with the following:

#import "cocos2d.h"
 
@class HelloWorldLayer;
 
#define kMaxHillKeyPoints 1000
 
@interface Terrain : CCNode {
    int _offsetX;
    CGPoint _hillKeyPoints[kMaxHillKeyPoints];
    CCSprite *_stripes;
}
 
@property (retain) CCSprite * stripes;
- (void) setOffsetX:(float)newOffsetX;
 
@end

This declares an array called _hillKeyPoints where we’ll store all of the points representing the peak of each hill, and an offset for how far the terrain is currently being scrolled.

Next we’re going to start implementing Terrain.m. I’m going to explain it step by step, so go ahead and delete everything currently in Terrain.m and add the following code section by section.

#import "Terrain.h"
#import "HelloWorldLayer.h"
 
@implementation Terrain
@synthesize stripes = _stripes;
 
- (void) generateHills {
 
    CGSize winSize = [CCDirector sharedDirector].winSize;    
    float x = 0;
    float y = winSize.width / 2;
    for(int i = 0; i < kMaxHillKeyPoints; ++i) {
        _hillKeyPoints[i] = CGPointMake(x, y);
        x += winSize.width/2;
        y = random() % (int) winSize.height;
    }
 
}

This is a method to generate the key points for some random hills. This is an extremely simple implementation just so we can have a starting point.

The first point is the left-side of the screen, in the middle along the y-axis. Each point after that moves half the width of the screen along the x-axis, and is set to a random value along the y-axis, from 0 up to the height of the screen.

- (id)init {
    if ((self = [super init])) {
        [self generateHills];
    }
    return self;
}
 
- (void) draw {
 
    for(int i = 1; i < kMaxHillKeyPoints; ++i) {    
        ccDrawLine(_hillKeyPoints[i-1], _hillKeyPoints[i]);        
    }
 
}

The init method calls generateHills to set up the hills, and the draw method simply draws lines between each of the points for debugging, so we can easily visualize them on the screen.

- (void) setOffsetX:(float)newOffsetX {
    _offsetX = newOffsetX;
    self.position = CGPointMake(-_offsetX*self.scale, 0);
}
 
- (void)dealloc {
    [_stripes release];
    _stripes = NULL;
    [super dealloc];
}
 
@end

Think about how the terrain moves – as our hero advances along the x-axis of the terrain, the terrain is sliding to the left. So we have to multiply the offset by -1 here – and don’t forget to take into consideration the scale!

Almost time to test this. Switch to HelloWorldLayer.h and make the following changes:

// Add to top of file
#import "Terrain.h"
 
// Add inside @interface
Terrain * _terrain;

Then switch to HelloWorldLayer.m and make the following changes:

// Add inside init BEFORE call to genBackground
_terrain = [Terrain node];
[self addChild:_terrain z:1];
 
// Add at bottom of update
[_terrain setOffsetX:offset];
 
// Modify genBackground to the following
- (void)genBackground {
 
    [_background removeFromParentAndCleanup:YES];
 
    ccColor4F bgColor = [self randomBrightColor];
    _background = [self spriteWithColor:bgColor textureSize:512];
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
    _background.position = ccp(winSize.width/2, winSize.height/2);        
    ccTexParams tp = {GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_REPEAT};
    [_background.texture setTexParameters:&tp];
 
    [self addChild:_background];
 
    ccColor4F color3 = [self randomBrightColor];
    ccColor4F color4 = [self randomBrightColor];
    CCSprite *stripes = [self stripedSpriteWithColor1:color3 color2:color4 textureSize:512 stripes:4];
    ccTexParams tp2 = {GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_CLAMP_TO_EDGE};
    [stripes.texture setTexParameters:&tp2];
    _terrain.stripes = stripes;
 
}

Note this sets the stripes texture on the Terrain to a new random stripes texture each time you tap, which is handy for testing.

Also, when calling setTextureRect on _background in update, you might wish to multiply the offset by 0.7 to get the background to scroll slower than the terrain.

And that’s it! Compile and run your code, and now you should see some lines drawn across the scene representing where the tops of your hills will eventually be:

Basic hill keypoints with debug drawing

As you watch your hills scroll by, you’ll probably realize pretty quickly that these wouldn’t work very well for a Tiny Wings game. Due to picking the y-coordinate randomly, sometimes the hills are too big and sometimes they are too small. There’s also not enough variance in the x-axis.

But now that you have this test code working and a good way to visualize and debug it, it’s simply a matter of dreaming up a better algorithm!

You can either take a few moments and come up with your own hill algorithm, replacing the code in generateHills, or you can use Sergey’s implementation, shown in the next section!

A Better Hills Algorithm

If you choose to use Sergey’s implementation, replace generateHills in Terrain.m with the following:

- (void) generateHills {
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
 
    float minDX = 160;
    float minDY = 60;
    int rangeDX = 80;
    int rangeDY = 40;
 
    float x = -minDX;
    float y = winSize.height/2-minDY;
 
    float dy, ny;
    float sign = 1; // +1 - going up, -1 - going  down
    float paddingTop = 20;
    float paddingBottom = 20;
 
    for (int i=0; i<kMaxHillKeyPoints; i++) {
        _hillKeyPoints[i] = CGPointMake(x, y);
        if (i == 0) {
            x = 0;
            y = winSize.height/2;
        } else {
            x += rand()%rangeDX+minDX;
            while(true) {
                dy = rand()%rangeDY+minDY;
                ny = y + dy*sign;
                if(ny < winSize.height-paddingTop && ny > paddingBottom) {
                    break;   
                }
            }
            y = ny;
        }
        sign *= -1;
    }
}

The strategy in this algorithm is the following:

  • Increment x-axis in the range of 160 + a random number between 0-40
  • Increment y-axis in the range of 60 + a random number between 0-40
  • Except: reverse the y-axis offset every other time.
  • Don’t let the y value get too close to the top or bottom (paddingTop, paddingBottom)
  • Start offscreen to the left, and hardcode the second point to (0, winSize.height/2), so there’s a hill coming up from the left offscreen.

Compile and run, and now you’ll see a much better hill algorithm, that looks like maybe a properly motivated seal might be able to fly off these!

Better hill keypoints
[HillsBetter.jpg]

Drawing Part at a Time

Before we go much further, we need to make a major performance optimization. Right now, we’re drawing all 1000 key points of the hills, even though only a few of them are visible on the screen at once!

So we could save a lot of time by simply calculating which key points to display based on the screen area, and display just those, as you can see below:

Optimization to draw visible points only

Let’s try this out. Start by adding two instance variables to Terrain.h:

int _fromKeyPointI;
int _toKeyPointI;

Then add a new method called resetHillVertices above the init method in Terrain.h that looks like the following:

- (void)resetHillVertices {
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
 
    static int prevFromKeyPointI = -1;
    static int prevToKeyPointI = -1;
 
    // key points interval for drawing
    while (_hillKeyPoints[_fromKeyPointI+1].x < _offsetX-winSize.width/8/self.scale) {
        _fromKeyPointI++;
    }
    while (_hillKeyPoints[_toKeyPointI].x < _offsetX+winSize.width*9/8/self.scale) {
        _toKeyPointI++;
    }
 
}

Here we loop through each of the key points (staring with 0) and look at their x-coordinates.

Whatever the current offset is set to maps to the left edge of the screen. So we take that and subtract the winSize.width/8. If the value is less than that, we keep advancing till we find one that’s greater. That’s our from keypoint, and we follow a similar process for the toKeypoint.

Now let’s see if this works! Modify your draw method like the following:

- (void) draw {
 
    for(int i = MAX(_fromKeyPointI, 1); i <= _toKeyPointI; ++i) {
        glColor4f(1.0, 0, 0, 1.0); 
        ccDrawLine(_hillKeyPoints[i-1], _hillKeyPoints[i]);        
    }
 
}

Now instead of drawing all of the points, we only draw the visible ones by using the indices we calculated earlier. We also change the color of the line to red to make it a bit easier to see.

Next make a few more mods to Terrain.m to call resetHillVertices:

// Add at bottom of init
[self resetHillVertices];
 
// Add at bottom of setOffsetX
[self resetHillVertices];

One more thing – to make this easy to see, go to the bottom of your init method in HelloWorldLayer.mm and add the following:

self.scale = 0.25;

Compile and run your code, and you should see the line segments pop in when it is time for them to be drawn!

Screenshot of game now drawing visible points only

Making Smooth Slopes

So far so good, but we have one big problem – those don’t look like hills at all! In real life, hills don’t go up and down in straight lines – they have slopes.

But how can we make our hills curved? Well one way to do it is with our friend cosine that we learned about back in high school!

As a quick refresher, here’s what a cosine curve looks like:

Diagram of a cosine curve

So it starts at 1, and every PI it curves down to -1.

But how can we make use of this function to create a nice curve connecting the keypoints? Let’s think about just two of them, as shown in the diagram below:
Cosine function mapped to hill

First, we need to draw the line in segments, so we’ll create one segment every 10 points. Similarly, we want a complete cosine curve, so we can divide PI by the number of segments to get the delta angle at each point.

Then, we want to map cos(0) to the y-coordinate of p0, and cos(PI) to the y-coordinate of p1. To do this, we’ll call cos(angle), and multiply the result by half the distance between p1 and p0 (called ampl in the diagram).

Since cos(0) = 1 and cos(PI) = -1, this gives us ampl at p0 and -ampl at p1. We can add that to the position of the midpoint to give us the y-coordinate we need!

Let’s see what this looks like in code. First add the definition of the segment length to the top of Terrain.h:

#define kHillSegmentWidth 10

Then add the following to your draw method, right after the call to ccDrawLine:

glColor4f(1.0, 1.0, 1.0, 1.0);
 
CGPoint p0 = _hillKeyPoints[i-1];
CGPoint p1 = _hillKeyPoints[i];
int hSegments = floorf((p1.x-p0.x)/kHillSegmentWidth);
float dx = (p1.x - p0.x) / hSegments;
float da = M_PI / hSegments;
float ymid = (p0.y + p1.y) / 2;
float ampl = (p0.y - p1.y) / 2;
 
CGPoint pt0, pt1;
pt0 = p0;
for (int j = 0; j < hSegments+1; ++j) {
 
    pt1.x = p0.x + j*dx;
    pt1.y = ymid + ampl * cosf(da*j);
 
    ccDrawLine(pt0, pt1);
 
    pt0 = pt1;
 
}

This runs through the strategy we outlined in the diagram above. Take a minute and think through the code and make sure you understand how this works, because we’ll be building on this from here.

One last thing – we don’t need to zoom out anymore, so back in HelloWorldLayer.mm replace the scale in init back to 1.0:

self.scale = 1.0;

Compile and run, and now you should see a curvy line connecting the hills!

Debug drawing of hill slopes with cosine

Drawing the Hill

Now that we know how to get the curve representing the top of the hill, it’s fairly simple to write the code to draw the hill using the striped texture we generated in the last tutorial!

The plan is for each segment of the hill, we’ll compute the two triangles needed to render the hill, as you can see in the diagram below:

Drawing hills with an OpenGL triangle strip

We’ll also set the texture coordinate at each point. For the x-coordinate, we’ll simply divide it by the texture’s width (since the texture repeats). For the y-coordinate though, we’ll map the bottom of the hill to 0 and the top of the hill to 1, distributing the full height of the texture along the strip.

To implement this, first make a few modifications to Terrain.h:

// Add some new defines up top
#define kMaxHillVertices 4000
#define kMaxBorderVertices 800 
 
// Add some new instance variables inside the @interface
int _nHillVertices;
CGPoint _hillVertices[kMaxHillVertices];
CGPoint _hillTexCoords[kMaxHillVertices];
int _nBorderVertices;
CGPoint _borderVertices[kMaxBorderVertices];

Then add the following code at the bottom of resetHillVertices in Terrain.m:

if (prevFromKeyPointI != _fromKeyPointI || prevToKeyPointI != _toKeyPointI) {
 
    // vertices for visible area
    _nHillVertices = 0;
    _nBorderVertices = 0;
    CGPoint p0, p1, pt0, pt1;
    p0 = _hillKeyPoints[_fromKeyPointI];
    for (int i=_fromKeyPointI+1; i<_toKeyPointI+1; i++) {
        p1 = _hillKeyPoints[i];
 
        // triangle strip between p0 and p1
        int hSegments = floorf((p1.x-p0.x)/kHillSegmentWidth);
        float dx = (p1.x - p0.x) / hSegments;
        float da = M_PI / hSegments;
        float ymid = (p0.y + p1.y) / 2;
        float ampl = (p0.y - p1.y) / 2;
        pt0 = p0;
        _borderVertices[_nBorderVertices++] = pt0;
        for (int j=1; j<hSegments+1; j++) {
            pt1.x = p0.x + j*dx;
            pt1.y = ymid + ampl * cosf(da*j);
            _borderVertices[_nBorderVertices++] = pt1;
 
            _hillVertices[_nHillVertices] = CGPointMake(pt0.x, 0);
            _hillTexCoords[_nHillVertices++] = CGPointMake(pt0.x/512, 1.0f);
            _hillVertices[_nHillVertices] = CGPointMake(pt1.x, 0);
            _hillTexCoords[_nHillVertices++] = CGPointMake(pt1.x/512, 1.0f);
 
            _hillVertices[_nHillVertices] = CGPointMake(pt0.x, pt0.y);
            _hillTexCoords[_nHillVertices++] = CGPointMake(pt0.x/512, 0);
            _hillVertices[_nHillVertices] = CGPointMake(pt1.x, pt1.y);
            _hillTexCoords[_nHillVertices++] = CGPointMake(pt1.x/512, 0);
 
            pt0 = pt1;
        }
 
        p0 = p1;
    }
 
    prevFromKeyPointI = _fromKeyPointI;
    prevToKeyPointI = _toKeyPointI;        
}

Much of this will look familiar, since we already covered it in the previous section when drawing the hill curve with cosine.

The new part is we’re now filling up an array with the vertices for each segment of the hill, as explained in the strategy part above. Each strip requires four vertices and four texture coords.

With that in place, you can now add the following code to the top of your draw method to make use of it:

glBindTexture(GL_TEXTURE_2D, _stripes.texture.name);
glDisableClientState(GL_COLOR_ARRAY);
 
glColor4f(1, 1, 1, 1);
glVertexPointer(2, GL_FLOAT, 0, _hillVertices);
glTexCoordPointer(2, GL_FLOAT, 0, _hillTexCoords);
glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)_nHillVertices);

This binds the stripes texture as the texture to use, passes in the array of vertices and texture coordinates made earlier, and draws the arrays as a triangle strip.

Also, you should comment out the lines of code that draw the hill lines and curve, since you’re about to see something awesome and don’t want debug drawing to spoil it! :]

Compile and run your code, and you should now see some awesome looking hills!

Awesome looking hills

Imperfections?

If you look closely at the hills, you may notice some imperfections such as this:

Imperfections in hill texture mapping

Some people in the Cocos2D forums seem to indicate that you can get this problem to go away by adding more vertical segments (instead of the 1 segment we have now).

However, I’ve personally found that while adding vertical segments doesn’t help the quality, increasing the horizontal segments does. Open up Terrain.h and modify kHillSegmentWidth like the following:

#define kHillSegmentWidth 5

Run again, and you’ll see your hills look much better! Of course, the tradeoff is processing time.

Totally beautiful hills

Where To Go From Here?

Here is a sample project with all of the code from the above tutorial.

Next check out part 2 of this tutorial, where finally we’ll add some gameplay code so you can make one lucky seal start to fly!


iPhoneCategory:

Tags: , , , , , ,

I'd love to hear your thoughts!