Hill Climb Racing – Deconstructed – Car, controls, camera, coins and fuel cans

Hello,

in the last chapter we went through on how to create terrain and now we will focus on creating four main things we now lack. We will start by creating car which can be driven with reverse and accelerate pedals. In order to keep the car centered on the screen, we will create camera that will act similarly to Hill Climb Racing which means it will zoom out while driving fast and zoom in when slowing down. The last thing we will go through is collecting coins and fuel cans. So we will have 4 simple concepts: Car, controls, camera, coins and fuel cans where each of these concepts one has some special features which we will go through.

I will again provide you with some template code, which we will start implementing as we go on and in the end you should be able to enjoy some fast driving while collecting dozens of coins and fuel cans.

What we have after this

1. Car
2. Controls
3. Camera
4. Coins and fuel cans

Download resources

1. Car class


Car physics with Box2D

We will start by creating car on our track. Because our car will be driven on the track which is made of Box2D body, the car will be made of Box2D body too, so the bodies can interact between each other. We will then add sprites on those bodies created and then update those sprite positions in update loop to make sprites follow Box2D bodies properly.

I won’t actually be going that much into detail on how to create car with Box2D bodies, because creating physics that provide well functioning car model is somewhat trial and error process. So the variables defined below just need to be tweaked long enough to have it work well enough for your purposes.
See these two links if want to get some ideas:
http://www.emanueleferonato.com/2009/04/06/two-ways-to-make-box2d-cars/
http://www.iforce2d.net/b2dtut/

First of all we need to create separate config.h file so that we can refer PTM_RATIO definition each time we need it.

#ifndef __CONFIG_H__
#define __CONFIG_H__

#define PTM_RATIO 10

#endif

Update Terrain.h code

#include "config.h"
//#define PTM_RATIO 32

Replace Terrain::update code with next code

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

Lets now build the pickup.
Car.h

#ifndef __CAR_H__
#define __CAR_H__

#include <Box2d/Box2D.h>
#include "cocos2d.h"

USING_NS_CC;

class Car {
private:
    bool _accelerating;
    bool _reversing;

    float _speed;
    float _torque;
    
    Sprite* _bodySprite;
    Sprite* _frontTireSprite;
    Sprite* _driverSprite;
    Sprite* _rearTireSprite;
    
    b2Body* _frontTireBody;
    b2Body* _frontAxelTireBody;
    b2Body* _rearTireBody;
    b2Body* _rearAxelTireBody;
    b2Body* _body;
    b2Body* _headBody;
    
    b2RevoluteJoint* _rearTireRevoluteJoint;
    b2PrismaticJoint* _rearTirePrismaticJoint;
    b2RevoluteJoint* _frontTireRevoluteJoint;
    b2PrismaticJoint* _frontTirePrismaticJoint;
    b2RevoluteJoint* _neckRevoluteJoint;
public:
    Car();
    ~Car();
  
    void init(b2World* world, Node* parent, Point pos);
    
    void accelerate(bool accelerating);
    void reverse(bool reversing);

    //our camera class needs to get a target body which to follow
    b2Body* getBody() {
        return _body;
    }

    //for collision detection
    Sprite* getSprite() {
        return _bodySprite;
    }
    
    void update(float dt);
};

#endif

Car.cpp

#include "config.h"
#include "Car.h"

USING_NS_CC;

Car::Car() {

}

Car::~Car() {
    
}

void Car::accelerate(bool accelerating) {
    _accelerating = accelerating;
}

void Car::reverse(bool reversing) {
    _reversing = reversing;
}

void Car::update(float dt) {
    if(_accelerating) {
        _rearTireRevoluteJoint->SetMotorSpeed(_speed);
        _rearTireRevoluteJoint->SetMaxMotorTorque(_torque);
        _frontTireRevoluteJoint->SetMotorSpeed(_speed);
        _frontTireRevoluteJoint->SetMaxMotorTorque(_torque);
    }
    
    if(_reversing) {
        _rearTireRevoluteJoint->SetMotorSpeed(-_speed);
        _rearTireRevoluteJoint->SetMaxMotorTorque(_torque);
        _frontTireRevoluteJoint->SetMotorSpeed(-_speed);
        _frontTireRevoluteJoint->SetMaxMotorTorque(_torque);
    }
    
    if(!_reversing && !_accelerating) {
        _rearTireRevoluteJoint->SetMotorSpeed(0);
        _rearTireRevoluteJoint->SetMaxMotorTorque(0);
        _frontTireRevoluteJoint->SetMotorSpeed(0);
        _frontTireRevoluteJoint->SetMaxMotorTorque(0);
    }
}

void Car::init(b2World* world, Node* parent, Point pos) {
    _torque = 100.0f;
    _speed = 400.0f;
    
    _frontTireSprite = Sprite::create("images/tire.png");
    _rearTireSprite = Sprite::create("images/tire.png");
    _bodySprite = Sprite::create("images/van.png");
    _driverSprite = Sprite::create("images/head1.png");
   
    parent->addChild(_frontTireSprite);
    parent->addChild(_rearTireSprite);
    parent->addChild(_driverSprite);
    parent->addChild(_bodySprite);
    
    b2BodyDef bodyDef;
    bodyDef.position = b2Vec2(pos.x/PTM_RATIO, pos.y/PTM_RATIO);
    bodyDef.type = b2_dynamicBody;
    
    _body = world->CreateBody(&bodyDef);
    _body->SetUserData(_bodySprite);
    b2PolygonShape carShape;
    carShape.SetAsBox(70.0f/PTM_RATIO, 10.0f/PTM_RATIO);
    
    b2FixtureDef carFixtureDef;
    carFixtureDef.shape = &carShape;
    carFixtureDef.density = 0.1;
    carFixtureDef.friction = 1.0f;
    carFixtureDef.filter.categoryBits = 0x02;
    carFixtureDef.restitution = 0.2;
    carFixtureDef.filter.maskBits = 0x01;
    _body->CreateFixture(&carFixtureDef);
    
    b2BodyDef driverBodyDef;
    b2Vec2 position = _body->GetPosition();
    driverBodyDef.position.Set((position.x + 10.0f / PTM_RATIO),(position.y + 10.0f / PTM_RATIO));
    driverBodyDef.type = b2_dynamicBody;
    
    _headBody = world->CreateBody(&driverBodyDef);
    _headBody->SetUserData(_driverSprite);
    b2CircleShape headShape;
    headShape.m_radius = 16.0f/PTM_RATIO;
    b2FixtureDef headFixtureDef;
    headFixtureDef.shape = &headShape;
    headFixtureDef.density = 0.1f;
    headFixtureDef.friction = 1.0f;
    headFixtureDef.filter.categoryBits = 0x02;
    headFixtureDef.filter.maskBits = 0x01;
    _headBody->CreateFixture(&headFixtureDef);
    
    b2RevoluteJointDef headJointDef;
    headJointDef.enableLimit = true;
    headJointDef.lowerAngle = -0.1f;
    headJointDef.upperAngle = 0.1f;
    b2Vec2 neckPos = b2Vec2(_headBody->GetWorldCenter().x, _headBody->GetWorldCenter().y - 0.4f);
    headJointDef.Initialize(_body, _headBody, neckPos);
    _neckRevoluteJoint = (b2RevoluteJoint*)world->CreateJoint(&headJointDef);
    
    
    b2BodyDef rearTireBodyDef;
    rearTireBodyDef.position.Set((pos.x + -40.0f)/PTM_RATIO, (pos.y - 25.0f)/PTM_RATIO);
    rearTireBodyDef.type = b2_dynamicBody;
    
    _rearTireBody = world->CreateBody(&rearTireBodyDef);
    _rearTireBody->SetUserData(_rearTireSprite);
    b2CircleShape rearTireShape;
    rearTireShape.m_radius = 16.0f/PTM_RATIO;
    
    b2FixtureDef rearTireFixtureDef;
    rearTireFixtureDef.shape = &rearTireShape;
    rearTireFixtureDef.density = 0.15;
    rearTireFixtureDef.friction = 1.0f;
    rearTireFixtureDef.filter.categoryBits = 0x02;
    rearTireFixtureDef.restitution = 0.2;
    
    _rearTireBody->CreateFixture(&rearTireFixtureDef);
    b2BodyDef rearAxelBodyDef;
    rearAxelBodyDef.position.Set(_rearTireBody->GetWorldCenter().x, _rearTireBody->GetWorldCenter().y);
    rearAxelBodyDef.type = b2_dynamicBody;
    _rearAxelTireBody = world->CreateBody(&rearAxelBodyDef);
    b2PolygonShape rearAxelShape;
    rearAxelShape.SetAsBox(6.0f/PTM_RATIO, 12.0f/PTM_RATIO, b2Vec2_zero, -0.5);
    b2FixtureDef rearAxelFixtureDef;
    rearAxelFixtureDef.shape = &rearAxelShape;
    rearAxelFixtureDef.density = 0.01f;
    rearAxelFixtureDef.filter.categoryBits = 0x02;
    rearAxelFixtureDef.filter.maskBits = 2;

    _rearAxelTireBody->CreateFixture(&rearAxelFixtureDef);
    
    b2RevoluteJointDef rearWheelRevoluteJointDef;
    rearWheelRevoluteJointDef.enableMotor = true;
    rearWheelRevoluteJointDef.Initialize(_rearTireBody, _rearAxelTireBody, _rearTireBody->GetWorldCenter());
    _rearTireRevoluteJoint = (b2RevoluteJoint*)world->CreateJoint(&rearWheelRevoluteJointDef);
    
    b2PrismaticJointDef rearWheelPrismaticJointDef;
    rearWheelPrismaticJointDef.enableLimit = true;
    rearWheelPrismaticJointDef.enableMotor = true;
    rearWheelPrismaticJointDef.lowerTranslation = -0.3f/PTM_RATIO;
    rearWheelPrismaticJointDef.upperTranslation = 0.5f/PTM_RATIO;
    rearWheelPrismaticJointDef.Initialize(_body, _rearAxelTireBody, b2Vec2(_rearAxelTireBody->GetWorldCenter().x, _rearAxelTireBody->GetWorldCenter().y), b2Vec2(0, 2));
    _rearTirePrismaticJoint = (b2PrismaticJoint*)world->CreateJoint(&rearWheelPrismaticJointDef);
    
    b2BodyDef frontTireBodyDef;
    frontTireBodyDef.position.Set((pos.x + 49.0f)/PTM_RATIO, (pos.y - 24.0f)/PTM_RATIO);
    frontTireBodyDef.type = b2_dynamicBody;
    
    _frontTireBody = world->CreateBody(&frontTireBodyDef);
    _frontTireBody->SetUserData(_frontTireSprite);
    b2CircleShape frontTireShape;
    frontTireShape.m_radius = 16.0f / PTM_RATIO;
    
    b2FixtureDef frontTireFixtureDef;
    frontTireFixtureDef.shape = &frontTireShape;
    frontTireFixtureDef.density = 0.15;
    frontTireFixtureDef.friction = 1.0;
    frontTireFixtureDef.filter.categoryBits = 0x02;
    frontTireFixtureDef.restitution = 0.2;
    _frontTireBody->CreateFixture(&frontTireFixtureDef);

    b2BodyDef frontAxelBodyDef;
    frontAxelBodyDef.position.Set(_frontTireBody->GetWorldCenter().x, _frontTireBody->GetWorldCenter().y);
    frontAxelBodyDef.type = b2_dynamicBody;
    _frontAxelTireBody = world->CreateBody(&frontAxelBodyDef);
    b2PolygonShape frontAxelShape;
    frontAxelShape.SetAsBox(6.0f/PTM_RATIO, 12.0f/PTM_RATIO, b2Vec2_zero, 0.5);
    b2FixtureDef frontAxelFixtureDef;
    frontAxelFixtureDef.shape = &frontAxelShape;
    frontAxelFixtureDef.density = 0.01f;
    frontAxelFixtureDef.filter.categoryBits = 0x01;
    frontAxelFixtureDef.filter.maskBits = 2;
    
    _frontAxelTireBody->CreateFixture(&frontAxelFixtureDef);
    
    b2RevoluteJointDef frontWheelRevoluteJointDef;
    frontWheelRevoluteJointDef.enableMotor = true;
    frontWheelRevoluteJointDef.Initialize(_frontTireBody, _frontAxelTireBody, _frontTireBody->GetWorldCenter());
    _frontTireRevoluteJoint = (b2RevoluteJoint*)world->CreateJoint(&frontWheelRevoluteJointDef);
    
    b2PrismaticJointDef frontWheelPrismaticJointDef;
    frontWheelPrismaticJointDef.enableLimit = true;
    frontWheelPrismaticJointDef.enableMotor = true;
    frontWheelPrismaticJointDef.lowerTranslation = -0.3f/PTM_RATIO;
    frontWheelPrismaticJointDef.upperTranslation = 0.5f/PTM_RATIO;
    frontWheelPrismaticJointDef.Initialize(_body, _frontAxelTireBody, b2Vec2(_frontAxelTireBody->GetWorldCenter().x, _frontAxelTireBody->GetWorldCenter().y), b2Vec2(0, 2));
    _frontTirePrismaticJoint = (b2PrismaticJoint*)world->CreateJoint(&frontWheelPrismaticJointDef);
}

So the code above creates our car and it has accelerate and reverse methods, which we will later on connect to our controls. It has init method which takes b2World as a parameter, so that the bodies we created can be added to the physics world. The next parameter will be our Terrain layer where we will add each sprite(tires, car frame and head). The last parameter is position and we will be positioning our car on top of our track.

Lets now drop our car on the terrain. For some reason I still haven’t renamed HelloWorldScene to any other name, but we will now go with it. Add following code into HelloWorldScene.h.

HelloWorld.h

...
#include "Car.h"

class HelloWorld : public cocos2d::Layer {
private:
...
    Car* _car;
public:
...
    virtual void update(float dt);
...
bool HelloWorld::init() {
    ...
    const Point startPoint = Point(400, 500);
    _car = new Car();
    _car->init(_world, _terrain, startPoint);

    //When we call this method, our HelloWorld layer will start calling update method and we will there update sprite positions into body position
    scheduleUpdate();
    ...
}

Let’s now position sprites into Box2D body positions. Put this on bottom of HelloWorldScene.cpp.

void HelloWorld::update(float dt) {
    //keep Sprite positions in sync with Box2D's bodies
    for (b2Body * b = _world->GetBodyList(); b != NULL; b = b->GetNext()) {
        if (b->GetUserData() != NULL) {
            Node* node = (Node*)b->GetUserData();
            node->setPosition(Point(b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO));
            node->setRotation(-1 * CC_RADIANS_TO_DEGREES(b->GetAngle()));
        }
    }
   
    _car->update(dt);
    _world->Step(dt, 10, 10);
}

Now if you run the code, provided that you position your car properly(startPoint variable), the car should fall on the track. Because we need to make this more fun, lets add controls so we can interact with our car.

2. Controls class


Pedals with multitouch support for controlling the car

So now we will continue on working our controls, which means reverse and accelerate pedals. We will develop controls with multitouch support, which means that pedals won’t lock in case we have more than one finger on the screen and for each of the touches we will be monitoring their positions whether on the reverse/accelerate pedal or not.

#ifndef __CONTROLS_H__
#define __CONTROLS_H__

#include <Box2d/Box2D.h>
#include "cocos2d.h"

USING_NS_CC;

class Controls : public Layer  {
private:
    bool _reversing;
    bool _accelerating;
    
    Sprite* _accelerateSprite;
    Sprite* _accelerateDownSprite;
    Sprite* _brakeSprite;
    Sprite* _brakeDownSprite;
    
    Touch* _brakeTouch;
    Touch* _thrustTouch;
    
    void handleTouches(const std::vector<Touch*>& touches, Event* event);

    void accelerate(bool accelerating);
    void reverse(bool reversing);
    
    void onTouchesBegan(const std::vector<Touch*>& touches, Event *event);
    void onTouchesEnded(const std::vector<Touch*>& touches, Event *event);
    void onTouchesMoved(const std::vector<Touch*>& touches, Event *event);
public:
    bool isAccelerating() {
       return _accelerating;
    }

    bool isReversing() {
       return _reversing;
    }
    
    virtual bool init();
    CREATE_FUNC(Controls);
};

#endif

Controls.cpp

#include "Controls.h"

USING_NS_CC;

bool Controls::init() {
    if(!Layer::init()) {
        return false;
    }

    _brakeTouch = nullptr;
    
    _reversing = false;
    _accelerating = false;
    
    const Point leftPedalPosition = Point(100, 100);
    const Point rightPedalPosition = Point(Director::getInstance()->getVisibleSize().width - 100, 100);
    
    _brakeSprite = Sprite::create("images/brake.png");
    _brakeDownSprite = Sprite::create("images/brake_down.png");
    _brakeSprite->setPosition(leftPedalPosition);
    _brakeDownSprite->setPosition(leftPedalPosition);
    
    _accelerateSprite = Sprite::create("images/accelerate.png");
    _accelerateDownSprite = Sprite::create("images/accelerate_down.png");
    _accelerateSprite->setPosition(rightPedalPosition);
    _accelerateDownSprite->setPosition(rightPedalPosition);
    
    addChild(_brakeSprite);
    addChild(_brakeDownSprite);
    addChild(_accelerateSprite);
    addChild(_accelerateDownSprite);
    
    _brakeDownSprite->setVisible(false);
    _accelerateDownSprite->setVisible(false);

    auto listener = EventListenerTouchAllAtOnce::create();
    listener->onTouchesBegan = CC_CALLBACK_2(Controls::onTouchesBegan, this);
    listener->onTouchesMoved = CC_CALLBACK_2(Controls::onTouchesMoved, this);
    listener->onTouchesEnded = CC_CALLBACK_2(Controls::onTouchesEnded, this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
    
    return true;
}

void Controls::accelerate(bool accelerating) {
    _accelerating = accelerating;
    if(_accelerating) {
        _accelerateDownSprite->setVisible(true);
        _accelerateSprite->setVisible(false);
    } else {
        _accelerateDownSprite->setVisible(false);
        _accelerateSprite->setVisible(true);
    }
}

void Controls::reverse(bool reversing) {
    _reversing = reversing;
    if(_reversing) {
        _brakeDownSprite->setVisible(true);
        _brakeSprite->setVisible(false);
    } else {
        _brakeDownSprite->setVisible(false);
        _brakeSprite->setVisible(true);
    }
}

//all the incoming touches will go trough this method. <em>touches</em> array contains all current touches 
//on the screen and therefore we be looping through each of those and checking if one of those touches
//are on one or both of the pedals. Note that if one of those touch events has a code of //EventTouch::EventCode::ENDED, it means touch has ended and therefore no pedal is pressed.
void Controls::handleTouches(const std::vector<Touch*> &touches, Event* event) {
    bool reversing = false;
    bool accelerating = false;
    for(int i = 0; i < touches.size(); ++i) {
        if(_brakeSprite->boundingBox().containsPoint(touches[i]->getLocation()) && ((EventTouch*)event)->getEventCode() != EventTouch::EventCode::ENDED) {
            reversing = true;
        } else if(_accelerateSprite->boundingBox().containsPoint(touches[i]->getLocation()) && ((EventTouch*)event)->getEventCode() != EventTouch::EventCode::ENDED) {
            accelerating = true;
        }
    }
    reverse(reversing);
    accelerate(accelerating);
}

void Controls::onTouchesBegan(const std::vector<Touch*>& touches, Event* event) {
    handleTouches(touches, event);
}

void Controls::onTouchesEnded(const std::vector<Touch*>& touches, Event* event) {
    handleTouches(touches, event);
}

void Controls::onTouchesMoved(const std::vector<Touch*>& touches, Event* event) {
    handleTouches(touches, event);
}

Our Controls class has two ‘outputs’, isAccelerating() and isReversing(). We will connect these ‘outputs’ to our car’s accelerate and reverse methods after we have first initialised Controls class.

HelloWorld.h

...
#include "Controls.h"

class HelloWorld : public cocos2d::Layer {
private:
...
    Controls* _controls;
}

HelloWorld.cpp

bool HelloWorld::init() {
    ...
    _controls = Controls::create();
    _controls->setZOrder(1000.0f);
    addChild(_controls);
}

If you now run the code, you should se pedals on left and right of your screen which you can press down in whatever ways you want, but of course it won’t be moving our car because we haven’t connected controls to our car. Lets next monitor state of the Controls in HelloWorld::update method, where we can update car’s state according to Controls state.

void HelloWorld::update(float dt) {
   ...
   _car->accelerate(_controls->isAccelerating());
   _car->reverse(_controls->isReversing());
}

Now by running the code you should be able to drive the car back and forward, however there is no camera yet to follow the car. Lets do that next.

3. Camera class


Camera is pretty essential thing when game contains something that can move freely in the game area which is larger than the game window. This game would be pointless if there was no camera following the car. We will add little extra to the camera, by adding a feature that zooms in and out depending on the vehicle’s velocity.

Lets continue coding. We are using CarCamera name for the class so it won’t be mixed with Cocos2d-x’s Camera class. You could also use namespaces to solve this issue, but this will suit our purposes now.

CarCamera.h

#ifndef __CAMERA_H__
#define __CAMERA_H__

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

#include "config.h"

USING_NS_CC;

class CarCamera {
private:
    //we need separate Layer for scaling
    Layer* _scaleLayer;

    //this will be our Terrain node
    Node* _node;
    
    const float MaxZoom = 1.5f;
    const float MinZoom = 0.6f;
    const float ZoomSpeed = 0.001f;
    
    float _targetScale;
    float _zoomVelocity;
    
    b2Body* _targetBody;
    
    Point getOffset();
public:
    CarCamera(Layer* scaleLayer, Node* node);
    ~CarCamera();
    
    void setTarget(b2Body* target);
    void update(float dt);
};

#endif

CarCamera.cpp

#include "CarCamera.h"

USING_NS_CC;

CarCamera::CarCamera(Layer* scaleLayer, Node* node) : _scaleLayer(scaleLayer), _node(node), _targetBody(nullptr), _zoomVelocity(1.0f) {
}

CarCamera::~CarCamera() {
    
}

Point CarCamera::getOffset() {
    float offsetX = 0;
    float offsetY = 0;
    if(_targetBody) {
        //convert box2d body position into pixel units and add offset to position to center of the screen
        float halvedScreenWidth = Director::getInstance()->getVisibleSize().width / 2.0f;
        float halvedScreenHeight = Director::getInstance()->getVisibleSize().height / 2.0f;
        offsetX = -(_targetBody->GetPosition().x * PTM_RATIO) + halvedScreenWidth;
        offsetY = -(_targetBody->GetPosition().y * PTM_RATIO) + halvedScreenHeight;
    }
    return Point(offsetX, fmin(0, offsetY));
}

void CarCamera::setTarget(b2Body* targetBody) {
    _targetBody = targetBody;
}

void CarCamera::update(float dt) {
    Point targetOffset = getOffset();
    _node->setPosition(targetOffset);
    
    //if we have set a target body for the camera, it'll start following that body
    if(_targetBody) {
        float velocity = fabs(_targetBody->GetLinearVelocity().Length());
        float zoom = (velocity / 60.0f);
        
        _zoomVelocity = MaxZoom - zoom;
        
        if(_zoomVelocity > _scaleLayer->getScale()) {
            _zoomVelocity = _scaleLayer->getScale() + ZoomSpeed;
        } else if(_zoomVelocity < _scaleLayer->getScale()) {
            _zoomVelocity = _scaleLayer->getScale() + -ZoomSpeed;
        }
        
        _zoomVelocity = clampf(_zoomVelocity, MinZoom, MaxZoom);
        _scaleLayer->setScale(_zoomVelocity);
    }
}

Now we have set up the camera, now we need to take it into use and what it needs is to do an initialisation and updating. Therefore:

HelloWorldScene.h

...
#include "CarCamera.h"

class HelloWorld : public cocos2d::Layer {
private:
    ...
    CarCamera* _camera;
    Layer* _scaleLayer;

Note that _terrain node is added as a child to _scaleLayer, so comment out code addChild(_terrain). Scale layer is a little trick used to handle zoom in out without messing too much with the rendering of the terrain.

bool HelloWorld::init() {
    ...
    _scaleLayer = Layer::create();
    addChild(_scaleLayer);
    
    //addChild(_terrain);
    _terrain = Terrain::create();
    _scaleLayer->addChild(_terrain);

    _camera = new CarCamera(_scaleLayer, _terrain);
    _camera->setTarget(_car->getBody());
void HelloWorld::update(float dt) {
    ...
    _camera->update(dt);

Now if you run the code, camera should follow the pickup wherever it goes. Notice, that if you drive fast enough, rendering of the terrain should not cover entire screen. This can be exercise for you to solve. If it turns out to be too hard to solve, feel free to comment. 🙂

What is left for this chapter, is to create coins and fuel cans to be collected.

4. Coins and fuel cans


Coins and fuel cans can be collected.

So as already stated, this is last part of this chapter. We will create coin and fuel can sprites on top of the track properly separated in distance from each other. In addition to that we will add simple collision detection so we can identify if car hits those sprites. Fuel actually does not ever end nor coins does not actually sum up in total money, but those issues will be resolved in next chapters. Well, lets keep going.

Insert following code strip in Terrain.h just below #define fields. With this we will identify collisions between each of these types.

  
struct ItemType {
    enum type {
        Coin,
        Fuel,
        Car
    };
};

Also add this method in Terrain.h.

class Terrain : public Node {
public:
    ...
    void generateItems(std::vector<Sprite*>& sprites);

Terrain.cpp

void Terrain::generateItems(std::vector<Sprite*>& sprites) {
    //this is spacing between 5 sets of coins. If GroundStep definition in Terrain.h equals 20 for example
    //coinSpacing would mean 100 * 20 = 2000 pixels and the same goes for positioning coins and fuel cans below
    int coinSpacing = 100;
    int coinAmount = 5;
    Point distanceFromGround = Point(0.0f, 30.0f);
    
    int fuelIndex = 0;
    for(int i = coinSpacing; i < MaxXYPoints; i += coinSpacing) {
        fuelIndex++;
        for(int j = 0; j < coinAmount; ++j) {
            Sprite* coinSprite = Sprite::create("images/coin.png");
            coinSprite->setTag(ItemType::Coin);
            coinSprite->setPosition(_xyPoints[i + j * 3] + distanceFromGround);
            addChild(coinSprite);
            sprites.push_back(coinSprite);
            
            //in every 5th set of 5 coins a fuel can is positioned
            if(j == coinAmount - 1 && fuelIndex > 4) {
                Sprite* fuelCanSprite = Sprite::create("images/fuel_can.png");
                fuelCanSprite->setPosition(_xyPoints[i + j * 3] + distanceFromGround);
                fuelCanSprite->setTag(ItemType::Fuel);
                addChild(fuelCanSprite);
                sprites.push_back(fuelCanSprite);
                fuelIndex = 0;
            }
        }
    }
}

If you noticed, generateItems method takes a vector as a reference and that is because we will monitor collisions between coins and fuel cans in HelloWorld class.

HelloWorldScene.h

class HelloWorld : public cocos2d::Layer {
private:
    ...
    std::vector<Sprite*> _sprites;

Now the only thing to do, is to call generateItems method to populate the track and then monitor collisions in HelloWorld::update.

HelloWorldScene.cpp

bool HelloWorld::init() {
    ...
    _terrain->generateItems(_sprites);
}

void Game::update(float dt) {
    ...
    for(int i = 0; i < _sprites.size(); ++i) {
    if(_car->getSprite()->boundingBox().intersectsRect(_sprites[i]->boundingBox()) && _sprites[i]->isVisible()) {
        if(_sprites[i]->getTag() == ItemType::Coin || _sprites[i]->getTag() == ItemType::Fuel) {
            _sprites[i]->runAction(FadeTo::create(1.0f, 0));
            _sprites[i]->runAction(MoveBy::create(1.0f, Point(0, 50.0f)));
            _sprites[i]->setTag(-1);
        }
    }
}

By now you should be able to run the code and drive the pickup while collecting money as the camera follows you from a distance depending of the vehicle’s velocity.

In case you’ve come this far and have some questions related to this chapter, feel free to ask. Hopefully I was able to cover these topics well enough for you to have better understanding of these concepts.

How to develop Hill Climb Racing using Cocos2d-x

I decided to start writing a tutorial which covers creating a similar gameplay experience compared to Hill Climb Racing, which is very popular game for mobile devices and which in my opinion cover some essential aspects of developing games for limited mobile platforms. We will be developing Hill Climb Racing using Cocos2d-x v3.0 engine.

As the game provides endless levels, some developers might have been pondering how it can be achieved. Or how the engine sound is made as it changes pitch or how loading bar is done, or how game states should be loaded and saved. How tricks are identified and so on. The upcoming tutorial parts will answer all those questions and more if there are some specific requests and I have time.

I will provide all the necessary resources and try to keep programming concepts as clear as possible.

hr

I will divide this tutorial in 5 parts:

1. Terrain

2. Car, controls, camera, coins and fuel cans

3. Hud and trick monitoring

4. Audio

5. Preloading and loading bar

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!