Wednesday, February 14, 2018

Hill Climb Racing development tutorial using Cocos2dx

Hill Climb Racing is a very popular game in mobile devices. This game has very good features like endless levels, custom vehicles and terrain surface. As a game developer i will teach you how to develop this game.
I will develop Hill Climb Racing using Cocos2d-x v3.
Cocos2dx-x is open source and cross platform game engine. Its installation and usage is super easy and it come with mobile ready features.

In this part we will go through how to identify tricks and checkpoints in the game using collision listeners etc. All aforementioned will be displayed on a HUD so user gets the feedback properly.

Let’s start this chapter by creating HUD and after that we’ll implement all necessary functionality.

HUD

HUD is very essential part of a game providing necessary feedback for user interactions. It of course could be called with many other names, but term HUD is pretty well known among gamers.

Our HUD will eventually contain information for:
1. Distance from the start
2. Amount of fuel left
3. Amount of money
4. Airtime and flip reward feedback

Let’s add template code for the HUD. The method names are pretty much self explanatory.



Hud.h

#ifndef __HUD_H__
#define __HUD_H__
 
#include "cocos2d.h"
 
USING_NS_CC;
 
class Hud : public Layer  {
private:
    Label* _messageLabel;
    Label* _moneyLabel;
    Label* _nextCheckpointLabel;
     
    Sprite* _fuelTankSprite;
    Sprite* _fuelTankBarSprite;
    Sprite* _coinSprite;
    Sprite* _distanceSprite;
public:
    void showMessage(std::string message);
     
    void setFuel(float fuel);
    void setMoney(int money);
    void setMeters(int nextCheckpont, int meters);
     
    virtual bool init();
    CREATE_FUNC(Hud);
};
 
#endif

Hud.cpp

#include "Hud.h"
#include "SimpleAudioEngine.h"
 
USING_NS_CC;
 
bool Hud::init() {
    if(!Layer::init()) {
        return false;
    }
     
    int fuelDistanceFromTop = 70;
    int moneyDistanceFromTop = 120;
    int distanceDistanceFromTop = 30;
     
    Size s = Director::getInstance()->getVisibleSize();
     
    _fuelTankSprite = Sprite::create("images/hud/fuel_can.png");
    _fuelTankSprite->setPosition(Point(40, s.height - fuelDistanceFromTop));
    addChild(_fuelTankSprite);
     
    _fuelTankBarSprite = Sprite::create("images/hud/fuel_can_bar.png");
    _fuelTankBarSprite->setAnchorPoint(Point(0.0f, 0.5f));
    _fuelTankBarSprite->setPosition(Point(90, s.height - fuelDistanceFromTop));
    addChild(_fuelTankBarSprite);
     
    _coinSprite = Sprite::create("images/hud/coin.png");
    _coinSprite->setPosition(Point(40, s.height - moneyDistanceFromTop));
    addChild(_coinSprite);
     
    _distanceSprite = Sprite::create("images/hud/distance.png");
    _distanceSprite->setPosition(Point(40, s.height - distanceDistanceFromTop));
    addChild(_distanceSprite);
     
    _messageLabel = Label::createWithTTF("Checkpoint reached!", "fonts/arial.ttf", 60);
    _messageLabel->setRotation(10.0f);
    _messageLabel->setScale(0.5f);
    _messageLabel->setPosition(Point(s.width/2.0f + 100, s.height/2.0f + 50.0f));
    _messageLabel->setOpacity(0);
    addChild(_messageLabel);
     
    _moneyLabel = Label::createWithTTF("0", "fonts/arial.ttf", 60);
    _moneyLabel->setScale(0.4f);
    _moneyLabel->setAnchorPoint(Point(0.0f, 0.5f));
    _moneyLabel->setPosition(Point(90, s.height - moneyDistanceFromTop));
    addChild(_moneyLabel);
     
    _nextCheckpointLabel = Label::createWithTTF("0/0", "fonts/arial.ttf", 60);
    _nextCheckpointLabel->setScale(0.4f);
    _nextCheckpointLabel->setAnchorPoint(Point(0.0f, 0.5f));
    _nextCheckpointLabel->setPosition(Point(90, s.height - distanceDistanceFromTop));
    addChild(_nextCheckpointLabel);
     
    return true;
}
 
void Hud::showMessage(std::string message) {
    _messageLabel->setString(message);
    Director::getInstance()->getActionManager()->removeAllActionsFromTarget(_messageLabel);
    _messageLabel->setOpacity(255);
    DelayTime* delay = DelayTime::create(0.5f);
    FadeOut* fade = FadeOut::create(0.2f);
    Sequence* seq = Sequence::create(delay, fade, NULL);
    _messageLabel->runAction(seq);
}
 
void Hud::setMeters(int nextCheckpont, int meters) {
    __String* str = String::createWithFormat("%d/%d", meters, nextCheckpont);
    _nextCheckpointLabel->setString(str->getCString());
}
 
void Hud::setFuel(float value) {
    const float barMaxWidth = 120;
    value = clampf(value, 0.0f, 120.0f);
    _fuelTankBarSprite->setScaleX((value * 1.2f) / barMaxWidth);
}
 
void Hud::setMoney(int money) {
    __String* str = String::createWithFormat("%d", money);
    _moneyLabel->setString(str->getCString());
}

Now we need to add the Hud layer as a child of our main layer by doing the following:

HelloWorldScene.h

#include "Hud.h"
class HelloWorld : public cocos2d::Layer {
private:
    ...
    Hud* _hud;

Initialize _hud variable and add is as a child just before end of bool HelloWorld::init so that HUD will be displayed on top of other layers. We could of course change the rendering order by changing z order, but adding layers in ‘correct’ order suffices here.

HelloWorldScene.cpp

_hud = Hud::create();
addChild(_hud);

Now if you compile and run the code you should see distance, fuel and money indicators in top left corner. Now we have our HUD frame which we can utilize later on with methods showMessage, setMeters, setFuel and setMoney

Car

Next we need to add some new features in our car class. Since our HUD needs a way to display car fuel, we need to add fuel variable in the class along with getAngle method which we use later on to identify flips.

Position method is for getting distance later on.

Car.h

class Car {
private:
    ...
    float _fuel;
public:
    ...
 
    float getAngle() {
        return _body->GetAngle();
    }
 
    float getFuel() {
        return _fuel;
    }
 
    Point getPosition() {
        return Point(_body->GetPosition().x * PTM_RATIO, _body->GetPosition().y * PTM_RATIO);
    }
     
    void setFuel(float fuel) {
        _fuel = fuel;
    }

When fuel value is <= 0 the fuel bar is empty.

Car.cpp

void Car::update(float dt) { 
   ... 
   _fuel -= 0.1f; 
}


HelloWorldScene.cpp

bool HelloWorld::init() {
    ...._car = new Car();
    _car - > init(_world, _terrain, _startPosition);
    _car - > setFuel(100.0 f);...
}
void HelloWorld::update(float dt) {
    ..._hud - > setFuel(_car - > getFuel());
    _camera - > update(dt);
}

Checkpoints

Now it’s time to add checkpoints functionality. Checkpoints are reached after certain amount of distance is driven. We can store distances in simple integer array. Also in order to know the distance, we need to add method for doing that which we later on compare with the checkpoint integer array.

Lets decide that 10 pixels equal on meter. Therefore in code below first checkpoints appears after 500 meters(5000 pixels).

Notice that we are adding car’s start position as a private variable _startPosition. We use this for calculating distance from start point forwards.

HelloWorldScene.h

...
private:
    int _currentCheckpoint;
    std::vector<int> _checkpoints;
 
    int getMeters();
    void nextCheckpoint();
 
    Point _startPosition;

HelloWorldScene.cpp

bool HelloWorldScene::init() {
    _startPosition = Point(400, 500);
    _car = new Car();
    _car->init(_world, _terrain, _startPosition);
     
    _currentCheckpoint = 0;
    _checkpoints.push_back(500);
    _checkpoints.push_back(1000);
    _checkpoints.push_back(1700);
    _checkpoints.push_back(2500);
    _checkpoints.push_back(3500);
    _checkpoints.push_back(5000);
}
 
int HelloWorld::getMeters() {
    const int meterEqualsPixels = 10;
    int totalMeters = (_car->getPosition().x - _startPosition.x) / meterEqualsPixels;
    //use std::max to avoid returning negative meters
    return std::max(totalMeters, 0);
}
 
void HelloWorld::nextCheckpoint() {
   _currentCheckpoint++;
   _hud->showMessage("Checkpoint reached!");
}
 
void HelloWorld::update(float dt) {
   ...
   //monitor whether player has passed previous checkpoint
   int currentMeters = getMeters();
   if(currentMeters >= _checkpoints[_currentCheckpoint]) {
        nextCheckpoint();
   }
 
   _hud->setMeters(_checkpoints[_currentCheckpoint], getMeters());
 
   _camera->update(dt);
   ...
}

Notice that the _currentCheckpoint variable is increased each time checkpoint is reached, which means that the if comparison will then start comparing against next position in _checkpoints array. Pretty straightforward.

Compile the code and take your car for a ride. After 5000 pixels you should get a “Checkpoint reached” displayed on HUD.

Fuel cans and coins

It is now time to do some preliminary actions before diving into tricks. We need to be able to identify between sprite objects of different type. For that purpose add following code in config.h. Before compiling remove ItemType struct enum from Terrain.h!

Config.h
...
#define PTM_RATIO 10
 
struct ItemType {
    enum type {
        Coin,
        Fuel,
        Head,
        Ground,
        Car
    };
};
...


Lets take those types into use. Add following code inside Car::init after sprites are initialized.

Car.cpp

_driverSprite->setTag(ItemType::Head);
_bodySprite->setTag(ItemType::Car);
_frontTireSprite->setTag(ItemType::Car);
_rearTireSprite->setTag(ItemType::Car);

Same thing with Terrain class’s fuel and coin sprites. Insert code

Terrain.h

...
private:
   Node* _groundNode;

Terrain.cpp

Terrain::Terrain() {
   ...
   _groundNode = Node::create();
   _groundNode->setTag(ItemType::Ground);
   addChild(_groundNode);
}
 
void TerrainG::initWorld(b2World* world) {
   ...
   _body = world->CreateBody(&bd);
   //with _groundNode we can identify if collision is happening with ground (ItemType::Ground).
   _body->SetUserData(_groundNode);
}
 
void Terrain::generateItems(std::vector<Sprite*>& sprites) {
...
   Sprite* coinSprite = Sprite::create("images/game/coin.png");
   coinSprite->setTag(ItemType::Coin);
...
   Sprite* fuelCanSprite = Sprite::create("images/game/fuel_can.png");
   fuelCanSprite->setTag(ItemType::Fuel);
...
}

Now we are able to identify collision between different kinds of objects and can more easily do some specific actions required.

Lets utilize HUD class when collecting money or fuel.

HelloWorldScene.h

private:
...
    int _money;
 
    //items
    void playCollectAnim(Sprite* sprite);
    void collectFuelTank(Sprite* sprite);
    void collectMoney(Sprite* sprite);

HelloWorldScene.cpp

void HelloWorld::playCollectAnim(Sprite* sprite) {
    sprite->runAction(MoveBy::create(0.5f, Point(0.0f, 40.0f)));
    sprite->runAction(FadeOut::create(0.5f));
}
 
void HelloWorld::collectFuelTank(Sprite *sprite) {
    _car->setFuel(120);
    playCollectAnim(sprite);
}
 
void HelloWorld::collectMoney(Sprite *sprite) {
    _money += 100;
    _hud->setMoney(_money);
    playCollectAnim(sprite);
}

Update HelloWorld::update code where collisions against fuel and coins are checked with following:

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) {
            collectMoney(_sprites[i]);
        } else if(_sprites[i]->getTag() == ItemType::Fuel) {
            collectFuelTank(_sprites[i]);
        }
        _sprites[i]->setTag(-1);
    }
}

Compile your code and collect some fuel cans and coins to see new features in action.

Tricks

Now we get to more hairy part of this chapter. There is many ways to identify flips and airtimes and this is only one among them.

What we essentially need is a way to know whether the car is on the ground or in the air. If car is in the air we monitor if car’s angle changes as much as is needed for a complete flip.

In order to monitor collisions using Box2d, we need to start by creating MyContactListener class, which Box2d fills with collision information for us to utilize. So create new files MyContactListener.h accordingly.

MyContactListener.h.

#ifndef __MYCONTACTLISTENER_H__
#define __MYCONTACTLISTENER_H__
 
#import <Box2d/Box2D.h>
#import <vector>
#import <algorithm>
 
#include "cocos2d.h"
 
USING_NS_CC;
 
struct MyContact {
    b2Body *bodyA;
    b2Body *bodyB;
    float impulseA;
    float impulseB;
    bool operator==(const MyContact& other) const
    {
        return (bodyA == other.bodyA) && (bodyB == other.bodyB);
    }
     
    bool hasCollision(int typeA, int typeB) {
        bool collision = false;
        if(bodyA && bodyB) {
            Node* aNode = (Node*)bodyA->GetUserData();
            Node* bNode = (Node*)bodyB->GetUserData();
             
            if(aNode && bNode) {
                log("id %d", aNode->getTag());
                if((aNode->getTag() == typeA && bNode->getTag() == typeB) ||
                   (bNode->getTag() == typeA && aNode->getTag() == typeB)) {
                    collision = true;
                }
            }
        }
         
        return collision;
    }
};
 
class MyContactListener : public b2ContactListener {
     
public:
    std::vector<MyContact> beginContacts;
    std::vector<MyContact> endContacts;
     
    void BeginContact(b2Contact* contact) {
        MyContact myContact = { contact->GetFixtureA()->GetBody(), contact->GetFixtureB()->GetBody() };
        beginContacts.push_back(myContact);
    }
     
    void EndContact(b2Contact* contact) {
        MyContact myContact = { contact->GetFixtureA()->GetBody(), contact->GetFixtureB()->GetBody() };
        endContacts.push_back(myContact);
    }
     
};
 
#endif

Next thing we need is to utilize this class. Create _contactListener variable pointer in HelloWorldScene.h.

HelloWorldScene.h

private:
...
   //tricks
   float _aerialTimer;
   float _lastRotation;
   int _collisionPoints;
   int _flips;
 
   MyContactListener* _contactListener;
 
   void flip();


HelloWorldScene.cpp

bool HelloWorld::init() {
    ...
    //add after Box2d's _world variable is initialized
    _contactListener = new MyContactListener();
    _world->SetContactListener(_contactListener);
 
    //initialize variables
    _aerialTimer = 0.0f;
    _collisionPoints = 0;
    _flips = 0;
    _lastRotation = 0.0f;
}
 
void HelloWorld::flip() {
    _flips++;
    _money += 1000;
    _hud->setMoney(_money);
    _hud->showMessage("Flip!");
}
 
void HelloWorld::update(float dt) {
...
    std::vector<MyContact>& beginContacts = _contactListener->beginContacts;
    bool groundHit = false;
    for(int i = 0; i < beginContacts.size(); ++i) {
       if(beginContacts[i].hasCollision(ItemType::Ground, ItemType::Car)) {
            groundHit = true;
            _aerialTimer = 0.0f;
            _collisionPoints++;
        }
    }
     
    std::vector<MyContact>& endContacts = _contactListener->endContacts;
    for(int i = 0; i < endContacts.size(); ++i) {
        if(endContacts[i].hasCollision(ItemType::Ground, ItemType::Car)) {
            _collisionPoints--;
        }
    }
     
    endContacts.clear();
    beginContacts.clear();
     
    //in case where car is on the ground, there are 2 collision points because
    //both tires are touching the ground
    if(_collisionPoints == 0) {
        _aerialTimer += dt;
        //air score bonus after every second(1.0f)
        if(_aerialTimer >= 1.0f) {
            _hud->showMessage("Air score +100!");
            _aerialTimer = _aerialTimer - 1.0f;
        }
    }
     
    //compare new angle with last angle. If new angle is larger or smaller than PI*2
    //it means car has rotated a full circle.
    float angle = _car->getAngle();
    if(!groundHit) {
        if(angle > _lastRotation + M_PI * 2) {
            _lastRotation = _lastRotation + M_PI * 2;
            flip();
        } else if(angle < _lastRotation - M_PI * 2) {
            _lastRotation = _lastRotation - M_PI * 2;
            flip();
        }
    }
 
    _camera->update(dt);
...
}


Now if you hit compile and run you should start gaining some rewards by doing flips and airtimes.

I hope I didn’t miss anything relevant information with this chapter. Anyway if that was case, please don’t hesitate to comment. Thanks!

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.