How to develop Hill Climb Racing with Cocos2d-x – Terrain

Hi,

this is the first part of the How to develop Hill Climb Racing with Cocos2d-x tutorial. I will presume you have some basic knowledge about programming and of using Cocos2d-x v3.0 engine. I’ll try to follow good coding practises while also giving links to related topics as we progress. Let’s get straight to the point and start deconstruction.

Here’s what we’ll have after finishing this first tutorial:
Hill Climb - deconstructed

What is it made of?

Algorithm which creates (x, y) points in 2d space for our upcoming track. These points have been achieved by using sine and cosine functions to create cylindrical shapes. We will later on implement the code, but for clarity let’s go through these concepts one by one.

Physics will define the track which will eventually interact with a car, by keeping the car on top of the track. We will use Box2D for handling physics calculations.

OpenGL meshes are used for visualising the track. Meshes consist of polygons which consist of vertices. The grass on top of the track is a thin slice of mesh drawn on top of the ground mesh. These two meshes are separate, because we don’t want to stretch the grass layer along with the ground mesh.

What is the problem?
Because track is ‘endless’, it is a very long strip of track to draw and calculate physics to. In reality it doesn’t need to be really endless, but we can create array big enough to contain track for so far that the finish line virtually unreachable. If we’d create physics and draw meshes for the whole track, it would jam the CPU and GPU which is not what we want.

We can just generate enough x, y points during game startup in an array, which can be then used to draw and create physics while the player keeps driving forwards. In this tutorial we will be creating x, y points in every 20th pixel, which means iPad2 screen can be filled with track with only 52 keypoints(x, y) 1024px/20px ~= 52. So if we generate 5200 of these points, there will be 100 screenfuls of track to drive and so on.

Ok, so now I’ve introduced you the basic concepts; let’s start coding. I presume you know how to create HelloWorld Cocos2d-x project which on top we will be building this game. We will first write all the code and later implement it on HelloWorld template. Download necessary graphics resources below:

Download resources

1. Terrain class
2. X, Y points
3. Box2D Physics
4. OpenGL meshes and scrolling
5. Optimising
6. Implementation

1. Terrain class


Let’s create new class called Terrain in Classes folder. We need to inherit Node class because we will be adding all the sprites etc on this Node which acts as the terrain which scrolls when player drives.

So lets set up Terrain class with following template code. We will implement those unimplemented methods later on.

Terrain.h

#ifndef __TERRAIN_H__
#define __TERRAIN_H__

#include "cocos2d.h"
#include "config.h"

USING_NS_CC;

#define PTM_RATIO 32

#define GroundStep 20
#define MaxXYPoints 20000
#define MaxHillVertices 40000

class Terrain : public Node {
    private:
        int _nHillVertices;
        int _numKeyIndices;
        int _maxPhysicsPoints;
        
        Point _xyPoints[MaxXYPoints];
        
        Point _groundVertices[MaxHillVertices];
        Point _groundTexCoords[MaxHillVertices];
        Point _grassVertices[MaxHillVertices];
        Point _grassTexCoords[MaxHillVertices];
        
        Texture2D* _grassTexture;
        Texture2D* _groundTexture;
        
        std::list<b2Fixture*> _fixtures;
        
        //this body will contain all physics for the track
        b2Body* _body;
        b2World* _world;
        
        //cocos2d-x 3.0 requires OpenGL calls to be wrapped in CustomCommand
        CustomCommand _renderCmds[1];
    public:
        Terrain();
        ~Terrain();
        
        void initWorld(b2World* world);
        void destroy();
        void generateXYPoints();
        void generateMeshes();
        void updatePhysics(int prevKeyIndex, int newKeyIndex);
        
        void onDraw(const Mat4 &transform);
        virtual void draw(Renderer *renderer, const Mat4& transform, uint32_t flags);
        virtual void update(float dt);
        
        virtual bool init();
        CREATE_FUNC(Terrain);
    };
}

#endif

Terrain.cpp

#include <Box2D/Box2D.h>
#include "Terrain.h"

USING_NS_CC;

Terrain::Terrain() {
    _body = nullptr;

    _maxPhysicsPoints = Director::getInstance()->getVisibleSize().width/GroundStep;

    Sprite* groundSprite = Sprite::create("images/game/ground.png");
    groundSprite->retain();
    _groundTexture = groundSprite->getTexture();
    _groundTexture->setTexParameters( { GL_NEAREST, GL_NEAREST, GL_REPEAT, GL_REPEAT});
        
    Sprite* grassSprite = Sprite::create("images/game/grass.png");
    grassSprite->retain();
    _grassTexture = grassSprite->getTexture();
    _grassTexture->setTexParameters( { GL_NEAREST, GL_NEAREST, GL_REPEAT, GL_REPEAT});
        
    GLProgram *shaderProgram_ = ShaderCache::getInstance()->getGLProgram(GLProgram::SHADER_NAME_POSITION_TEXTURE);
        setGLProgram(shaderProgram_);
    scheduleUpdate();
}

Terrain::~Terrain() {

}

bool Terrain::init() {
    if(!Node::init()) {
        return false; 
    }
      
    return true;
}

void Terrain::initWorld(b2World* world) {
    _world = world;

    //set up our _body variable which will eventually contain the track
    b2BodyDef bd;
    bd.position.Set(0, 0);
    bd.type = b2_staticBody;
    _body = world->CreateBody(&bd);
}

void Terrain::update(float dt) {

}
       

X, Y points

xypoints

First of all we need an algorithm which generates all the required x, y points that we will use for building physics and graphics for our track. So let’s create new method called generateXYPoints()

//Generates x, y points every in every 20th x unit point
void Terrain::generateXYPoints() {
    float yOffset = 350.0f;
    
    int index = 0;
    for (int i = 0; i < MaxXYPoints; i++) {
        _xyPoints[i].x = index;
        _xyPoints[i].y = yOffset - sinf(i) * 60 * i / 100.0f;
        index += GroundStep;
    }
}

Now we have created 20000(MaxXYPoints) x, y points which will be the backbone of our track. If you want to play with the track design, this method is where it should be done. In case 20000 points is too low for your purposes, feel free to increase the MaxXYPoints definition in Terrain.h file.

Physics

physics
Cocos2d-x samples include GLES-Render class, which is a tool for drawing shapes of a physic objects. Copy GLES-Render.cpp and GLES-Render.h files from your Cocos2d-x directory, tests/cpp-tests/Classes/Box2DTestBed/ to your project’s Classes folder.

After you’ve done this, we may continue adding physics on our track. Now create method which takes startIndex and endIndex as parameters, which means we can define which portion of the track should physics be calculated. We will modify this method later on to optimise physics calculation properly.

void Terrain::updatePhysics(int startIndex, int endIndex) {
   b2EdgeShape shape;
   for (int i=prevKeyIndex; i < newKeyIndex; i++) {
       b2Vec2 p1 = b2Vec2((_xyPoints[i].x)/PTM_RATIO, _xyPoints[i ].y/PTM_RATIO);
       b2Vec2 p2 = b2Vec2((_xyPoints[i+1].x)/PTM_RATIO, _xyPoints[i +1].y/PTM_RATIO);
       shape.Set(p1, p2);
       _numKeyIndices++;
   }
}

OpenGL meshes

meshes

Now we have create X, Y points, which are already used for physics of our track. Next we need to use that same x, y information to generate visuals for the track.
So lets create meshes for the ground and grass. We loaded up textures earlier in the constructor of Terrain, so that part is done. Now we need to create vertices and texture coordinates. We need to create continuous set of polygons next to each other and we need to wrap a texture over and that is what we’re doing next.

void Terrain::generateMeshes() {
    _nHillVertices = 0;
    Point p;
    //generate thin slice of grass mesh on top of the ground mesh using X, Y points generated in generateXYPoints() method
    for (int i=0; i < MaxXYPoints; i++) {
        p = _xyPoints[i];
        _grassVertices[_nHillVertices] = Point(p.x, p.y - 12.0f);
        _grassTexCoords[_nHillVertices++] = Point(p.x/_grassTexture->getPixelsWide(), 1.0f);
        _grassVertices[_nHillVertices] = Point(p.x, p.y);
        _grassTexCoords[_nHillVertices++] = Point(p.x/_grassTexture->getPixelsWide(), 0);
    }
    
    _nHillVertices = 0;
    
    //generate mesh for ground below grass
    for (int i = 0; i < MaxXYPoints; i++) {
        p = _xyPoints[i];
        _groundVertices[_nHillVertices] = Point(p.x, 0);
        _groundTexCoords[_nHillVertices++] = Point(p.x/_groundTexture->getPixelsWide(), 0);
        _groundVertices[_nHillVertices] = Point(p.x, p.y - 5.0f);
        _groundTexCoords[_nHillVertices++] = Point(p.x/_groundTexture->getPixelsWide(), (p.y / _groundTexture->getPixelsHigh()));
    }
}

Now we are at a point where we have generated physics and meshes, so we are one step away from the end results, without the optimisations. We are missing drawing code so lets continue from that.

void Terrain::draw(Renderer* renderer, const Mat4 &transform, uint32_t flags) {
    Node::draw(renderer, transform, flags);

    _renderCmds[0].init(0.0f);
    _renderCmds[0].func = CC_CALLBACK_0(Terrain::onDraw, this, transform);
    renderer->addCommand(&_renderCmds[0]);
}
    void Terrain::onDraw(const Mat4 &transform) {
    auto glProgram = getGLProgram();
    glProgram->use();
    glProgram->setUniformsForBuiltins(transform);
    
    GL::bindTexture2D(_groundTexture->getName());
    GL::enableVertexAttribs( GL::VERTEX_ATTRIB_FLAG_POS_COLOR_TEX );
    
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_POSITION);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, 0, _groundVertices);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, 0, _groundTexCoords);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)MaxHillVertices);
    
    GL::bindTexture2D(_grassTexture->getName());
    GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POS_COLOR_TEX);
    
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_POSITION);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, 0, _grassVertices);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, 0, _grassTexCoords);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)MaxHillVertices);
    
    //set this flag to true to see physics debug draw in action
    static bool debugDraw = false;
    if(_world && debugDraw) {
        Director* director = Director::getInstance();
        director->pushMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
        director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW, transform);
        
        GL::enableVertexAttribs(cocos2d::GL::VERTEX_ATTRIB_FLAG_POSITION);
        _world->DrawDebugData();
        CHECK_GL_ERROR_DEBUG();
        
        director->popMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
    }
}

Now we have created nice physics and visuals for our track. Now we should be able to scroll along the road. Lets create simple code to handle that.

 
void Terrain::update(float dt) {
    //lets move the track -0.5 pixel at each update
    setPositionX(getPositionX() - 0.5f);
}

If you test this with a mobile platform you might encounter some performance issues. These issues arise from physics calculation and from OpenGL rendering that is done for the whole track at once. Because our track needs to be endless, this kind of solution doesn’t really scale up. If you are not observing these performance issues, tweak up these macros:

#define MaxXYPoints 10000
#define MaxHillVertices 20000

Optimising

ipad

glDrawArrays() method takes in the start index and how many triangles should be drawn and this is where we need to take into account the ‘camera’ position. At this point we don’t need Box2d’s debug draw, so we’ll remove those lines. Lets optimise onDraw method:

void Terrain::onDraw(const Mat4 &transform) {
    //here we calculate starting index where we start drawing
    int start = abs((getPositionX()) / GroundStep) * 2;
    //and how many points to draw. Both variables are multiplied by 2 because
    //ground and grass polygons have top and bottom points defined for one x value
    int numPoints = 1024.0f / GroundStep * 2;

    auto glProgram = getGLProgram();
    glProgram->use();
    glProgram->setUniformsForBuiltins(transform);

    GL::bindTexture2D( _groundTexture->getName() );
    GL::enableVertexAttribs( GL::VERTEX_ATTRIB_FLAG_POS_COLOR_TEX );

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_POSITION);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, 0, _groundVertices);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, 0, _groundTexCoords);
    glDrawArrays(GL_TRIANGLE_STRIP, start, (GLsizei)numPoints);
    
    GL::bindTexture2D(_grassTexture->getName() );
    GL::enableVertexAttribs( GL::VERTEX_ATTRIB_FLAG_POS_COLOR_TEX );
    
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_POSITION);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, 0, _grassVertices);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, 0, _grassTexCoords);
    glDrawArrays(GL_TRIANGLE_STRIP, start, (GLsizei)numPoints);
}

Physics optimization is little more tricky. We could probably tweak Box2Ds collision algorithm to avoid calculating of the track points which are not visible on the screen, but that sounds like too far fetched.
Our track’s B2Body contains b2EdgeShape fixtures, which are those lines that the GLES debug draws. There is a very performance cheap way to remove those lines one by one from the back and increase one by one on the end. It’s sort of like a moving worm in those early days Nokia phones. So let’s get to it.

Now lets modify physics updating code:

void Terrain::updatePhysics(int prevKeyIndex, int newKeyIndex) {
    b2EdgeShape shape;
    for (int i=prevKeyIndex; i < newKeyIndex; i++) {
        b2Vec2 p1 = b2Vec2((_xyPoints[i].x)/PTM_RATIO,_xyPoints[i].y/PTM_RATIO);
        b2Vec2 p2 = b2Vec2((_xyPoints[i+1].x)/PTM_RATIO,_xyPoints[i +1].y/PTM_RATIO);
        shape.Set(p1, p2);
        _fixtures.push_back(_body->CreateFixture(&shape, 0));
        _numKeyIndices++;
    }
    
    while(_fixtures.size() > _maxPhysicsPoints) {
        _body->DestroyFixture(_fixtures.front());
        _fixtures.pop_front();
    }
}

Now all we have left is to update track shape on each update call

void Terrain::update(float dt) {
    setPositionX(getPositionX() - 0.5f);
    
    int start = abs((getPositionX()) / GroundStep);
    updatePhysics(start, start + _maxPhysicsPoints);
}

optimized

If you want to verify this works, feel free to set different scale for the HelloWorldLayer and observe the results.

Implementation


HelloWorldScene.h

 
#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include <Box2d/Box2D.h>
#include "cocos2d.h"
#include "GLES-Render.h"
#include "Terrain.h"

class HelloWorld : public cocos2d::Layer {
private:
    GLESDebugDraw* _debugDraw;
    b2World* _world;
    LayerGradient* _backgroundGradientLayer;
    
    Terrain* _terrain;
public:
    // there's no 'id' in cpp, so we recommend returning the class instance pointer
    static cocos2d::Scene* createScene();

    // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
    virtual bool init();
    
    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);
};

#endif // __HELLOWORLD_SCENE_H__

HelloWorldScene.cpp

#include "HelloWorldScene.h"

USING_NS_CC;

Scene* HelloWorld::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();
    
    // 'layer' is an autorelease object
    auto layer = HelloWorld::create();

    // add layer as a child to scene
    scene->addChild(layer);

    // return the scene
    return scene;
}

// on "init" you need to initialize your instance
bool HelloWorld::init() {
    if (!Layer::init()) {
        return false;
    }

    Size size = Director::getInstance()->getVisibleSize();
    _backgroundGradientLayer = LayerGradient::create(Color4B(106, 164, 208, 255), Color4B(159, 202, 243, 255));
    addChild(_backgroundGradientLayer);
    
    _terrain = Terrain::create();
    addChild(_terrain);

    b2Vec2 gravity = b2Vec2(0.0f, -30.0f);
    bool doSleep = true;
    _world = new b2World(gravity);
    _world->SetAllowSleeping(doSleep);

    _terrain->initWorld(_world);
    _terrain->generateXYPoints();
    _terrain->generateMeshes();
    _terrain->updatePhysics(0, 1024);

    
    _debugDraw = new GLESDebugDraw(PTM_RATIO);
    _world->SetDebugDraw(_debugDraw);
    _debugDraw->SetFlags(GLESDebugDraw::e_shapeBit | GLESDebugDraw::e_jointBit);
    
    return true;
}

Now you should be able to compile this project and see how it looks. Keep tweaking the algorithm that creates the track, so you will have nice track to drive when we create controls, collectibles and car in next chapter!

If you ever read this far, thanks for reading! Feel free to comment and ask any questions you might have and I’ll be happy to answer!

39 Comments

  1. jonimikkola

    No problem. Just edit void Terrain::generateXYPoints() method, where you set your desired y value in _xyPoints[i].y variable. For example you could generate slight uphill by using this code: _xyPoints[i].y = yOffset + i * 0.5f;

  2. jonimikkola

    X value is set in _xyPoints[i].x. It needs to be incrementing because the track goes from left to right. If you want to add more length to the track, grow these values:
    #define MaxXYPoints 20000 -> 200000
    #define MaxHillVertices 40000 -> 400000
    Or whatever suits your purposes.

    1. jonimikkola

      Set different value in GroundStep variable defined in Terrain.h. By default it is 20, which means x points are set in every 20th pixel and if you set GroundStep=5, it means x points are set more closely each other making ground smoother. If you set it 1, it makes the ground as smooth as possible.

  3. himanshu

    thanks use like this
    #define GroundStep 1
    #define MaxXYPoints 20000
    #define MaxHillVertices 40000
    void Terrain::generateXYPoints() {
    float yOffset = 350.0f;

    int index = 0;
    for (int i = 0; i < MaxXYPoints; i++) {
    _xyPoints[i].x = index;
    _xyPoints[i].y = yOffset+rand()%10;
    index += GroundStep;
    }
    }

  4. himanshu

    yes but it only limited point and limited hill point so this is not real hill climb Terrain. hill climb Terrain is endless

    1. jonimikkola

      Hi, what do you mean exactly? Background image in this example is 1024×768 pixels, which means if game window is larger the background image doesn’t cover whole screen.

      1. zero

        Yes. The background image doesn’t cover whole screen. Probably something about screen resolution… I am trying to make it look better..

        1. jonimikkola

          Hi, I updated the article with a code that creates gradient layer(similar as the sprite before) which should cover the entire screen. Could you try that out?
          HelloWorldScene.h:
          //Sprite* _backgroundSprite;
          LayerGradient* _backgroundGradientLayer;

          HelloWorldScene.cpp
          //_backgroundSprite = Sprite::create(“images/background.png”);
          //addChild(_backgroundSprite);
          //_backgroundSprite->setPosition(Point(size.width/2.0f, size.height/2.0f));

          _backgroundGradientLayer = LayerGradient::create(Color4B(106, 164, 208, 255), Color4B(159, 202, 243, 255));
          addChild(_backgroundGradientLayer);

  5. Pingback: Hill Climb Racing - Deconstructed - Car, controls, camera, coins and fuel cans - All sorts of development

  6. jonimikkola

    Hi. How large terrain do you mean? I suppose you could use https://www.codeandweb.com/physicseditor/. I think one way to do a terrain using Physics Editor would be to first draw the terrain as a black and white image, then use the auto-tracing feature for tracing edges. But again if you are creating very large terrain this might not be suitable.

    1. jonimikkola

      Make these values higher in Terrain.h
      #define MaxXYPoints 20000 -> 200000
      #define MaxHillVertices 40000 -> 400000

      1. Michael

        Awesome. Thanks for the reply. Also, I haven’t fully read the article yet but is it Box2D or chipmunk? As I thought 3.0 has only chipmunk? Thanks

  7. sunil singh

    i have drawn this endless terrain.Now next target is to draw a bike on this terrain .For this i used Box2D and createe a bike body (chesis and wheels ).Box2d return me a .json file that i used to read the bodies from the world.After doing this my bike body successfully drew but started falling.Bike body didn’t hav any contact with grass or ground and fall directly.i used a single world for this.

  8. Hugo

    Can you put this source code on your Github? Please!? I am using cocos2d-x v3.10 and I couldn’t find config.h and every time that I try to compile I found so many bug’s that I could not even understand why…

    1. jonimikkola

      Hi Hugo, yes I can. The reason it is not in Github right now is that I thought people might learn better when guiding step by step. I will reply to you when it’s in Github.

      1. Hugo

        Hi Joni, I think that now I could understand what is that config.h file used for, this is a kind of an interface to constants in your project? I saw in other tutorials that you have used that file to something like this, I think that I’m learning much more without GutHub, you are right this way is much better, hard at least for me, but much better!

      2. Hugo

        Hi Joni, this tutorial has taught me a lot, today I finally could run your tutorial project, first I thought that my problem was that I don’t know about Box2d, them I learned Box2d a little, them I was thinking that I need to learn about OpenGL, after these months I could understand my bugs, the problem wasn’t in config.h, the problem was in the version of Cocos2d-x 3.10 that has another class called Terrain and cause more than 150 bugs about ambiguity, but I was worth, thank you master!

  9. George

    It seems to work fine, except the fps seem to drop as it takes more time to render. Why do you think this happens?

Leave a Reply

Your email address will not be published. Required fields are marked *