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.
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
Hud.cpp
Now we need to add the Hud layer as a child of our main layer by doing the following:
HelloWorldScene.h
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
When fuel value is <= 0 the fuel bar is empty.
Car.cpp
HelloWorldScene.cpp
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
HelloWorldScene.cpp
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
Lets take those types into use. Add following code inside Car::init after sprites are initialized.
Car.cpp
Same thing with Terrain class’s fuel and coin sprites. Insert code
Terrain.h
Terrain.cpp
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
HelloWorldScene.cpp
Update HelloWorld::update code where collisions against fuel and coins are checked with following:
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.
Next thing we need is to utilize this class. Create _contactListener variable pointer in HelloWorldScene.h.
HelloWorldScene.h
HelloWorldScene.cpp
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!
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
Position method is for getting distance later on.
Car.h
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!