We'll start by looking at the CreateGame
function that is called from the init
function when GameWorld
is created:
void GameWorld::CreateGame() { // player starts off with 30 seconds time_left_ = 30; // create & add the first set of hills at 0 terrain_ = Terrain::create(world_, 0); addChild(terrain_, E_LAYER_FOREGROUND); // create & add the sky sky_ = Sky::create(); addChild(sky_, E_LAYER_BACKGROUND); // create & add the penguin to the hills penguin_ = Penguin::create(this, "penguin_1.png"); terrain_->addChild(penguin_, E_LAYER_FOREGROUND); CreateHUD(); // enable touch and schedule two selectors; the seconds ticker and the update setTouchEnabled(true); schedule(schedule_selector(GameWorld::Tick), 1.0f); scheduleUpdate(); }
We begin this function by initializing a variable called time_left_
to 30
. This means that the player starts off with 30 seconds to take the penguin as far as possible. We then create the hills, sky, and penguin. Notice how the penguin is added to terrain_
instead of this
(GameWorld
). By doing this, we only need to move and scale the terrain_
object to simulate the movement of a camera following the penguin as he glides across the hill. The rest is boilerplate code that you've seen before. Let's look at the update
function from GameWorld
:
void GameWorld::update(float dt) { // update penguin physics penguin_->UpdatePhysics(); // update the world // slow it down...otherwise the game is too fast to enjoy! world_->Step(dt * 0.5f, 8, 3); // update penguin node penguin_->UpdateNode(); // update the hills and sky terrain_->Update(penguin_->getPosition()); sky_->Update(penguin_->getPosition(), terrain_->GetOffsetX(), terrain_->getScale()); // if penguin has gone beyond the second last key point, its time to take a leap if(penguin_->getPositionX() > terrain_->GetCliffKeyPoint().x && !penguin_->GetIsLeaping()) { penguin_->StartLeap(); } // check if the penguin is leaping first if(penguin_->GetIsLeaping()) { // if the next hill's foot is close enough, its time to stop leaping & time to start falling if(terrain_->GetFootKeyPoint().x > penguin_->getPositionX() && terrain_->GetFootKeyPoint().x - penguin_->getPositionX() <= SCREEN_SIZE.width * 1.75f) { penguin_->FinishLeap(); } } // update the distance counter & label int new_distance_travelled = penguin_->getPositionX(); if(new_distance_travelled > distance_travelled_) { char buf[16] = {0}; sprintf(buf, "%dm", (int)(new_distance_travelled / 2)); distance_label_->setString(buf); distance_travelled_ = new_distance_travelled; } }
We start this function off by calling the UpdatePhysics
function on the penguin. Then, we step into the Box2D world and call the penguin's UpdateNode
function, followed by the Update
functions of the Terrain
and Sky
classes respectively. Notice how we've halved the delta time there. This is to slow down the game as it is just too fast to play. Try it and see for yourself.
The next bit of code is important since it is here that we toggle the penguin's leaping state. First we check if the penguin has surpassed the terrain's cliff by calling the GetCliffKeyPoint
function on terrain_
. Then, we check whether the penguin isn't already leaping. As its name suggests, this function GetCliffKeyPoint
returns to us the key point of the cliff, which is nothing but the second last key point in the array hill_key_points_
from the Terrain
class.
Now that the penguin is leaping, we monitor the time when the leaping should be stopped. To know when to stop leaping, we need to know the location of the next foothill. That is exactly what GetFootKeyPoint
of Terrain
returns to us. Thus, our if
condition basically checks if there is the appropriate distance between the penguin and the next foothill before calling the FinishLeap
function of Penguin
. The last bit of code you see there is similar to what we saw in the previous chapter, and it is used to update the HUD to track the distance the penguin has travelled thus far.
We'll now take a look at the PreSolve
function, which is one of the callbacks b2ContactListener
provides to us. The PreSolve
function is called for a given contact before that contact goes into Box2D's solver. Here is the PreSolve
function from GameWorld.cpp
:
void GameWorld::PreSolve(b2Contact* contact, const b2Manifold* oldManifold) { // get the world manifold for this contact b2WorldManifold world_manifold; contact->GetWorldManifold(&world_manifold); // get velocity of the penguin b2Vec2 velocity = penguin_->GetBody()->GetLinearVelocity(); // get angles for the velocity & normal vectors float velocity_angle = atan2f(velocity.y, velocity.x); float normal_angle = atan2f(world_manifold.normal.y, world_manifold.normal.x); // it is a bad landing if the difference in angles is above a certain threshold if (normal_angle - velocity_angle > BAD_LANDING_ANGLE_DIFF) { penguin_->OnBadLanding(); } }
First, we get the b2WorldManifold
object for the current contact. The b2WorldManifold
structure provides a normal for the collision and an array containing the points where two fixtures have collided. Next, we record the linear velocity of the penguin's body. We then calculate two angles: the angle of the penguin's velocity vector and the angle of the contact's normal vector. Then, we specify that if the difference between these two angles is greater than a predetermined threshold (BAD_LANDING_ANGLE_DIFF
), the player has made a bad landing. BAD_LANDING_ANGLE_DIFF
holds a value in radians and can be found in GameGlobals.h
.
We're almost done with the gameplay. While defining the Penguin
class, there was some talk of time being added for a perfect slide and a leap and we also initialized the variable time_left_
in the CreateGame
function. It means that we must keep track of the time somewhere. This is done by the Tick
function of GameWorld.cpp
:
void GameWorld::Tick(float dt) { // tick only after the game has started till before it ends if(!has_game_begun_ || has_game_ended_) return; // decrement the time counter -- time_left_; // update the label char buf[16] = {0}; sprintf(buf, "%ds", time_left_); time_label_->setString(buf); // no more time...game over if(time_left_ <= 0) { GameOver(); } // when there 5 seconds or less, start animating the label & playing a sound if(time_left_ <= 5) { if(time_label_->numberOfRunningActions() <= 0) { CCActionInterval* scale_up_down = CCSequence::createWithTwoActions( CCScaleTo::create(0.2f, 1.2f), CCScaleTo::create(0.2f, 1.0f)); time_label_->runAction(CCRepeatForever::create(scale_up_down)); CCActionInterval* shake = CCSequence::createWithTwoActions( CCRotateBy::create(0.05f, 20.0f), CCRotateBy::create(0.05f, -20.0f)); time_label_->setRotation(-10.0f); time_label_->runAction(CCRepeatForever::create(shake)); } } }
We want this function to be processed only after the game has begun and before it has ended; hence, the if
condition. We then decrement the time_left_
variable, which keeps track of the amount of time the player has remaining. Then, we update the HUD to reflect the time left and call GameOver
when the player has run out of time. Also, to alert the user, we animate the time label when time left is 5 seconds or less.
This winds up the Tick
function and also completes our sixth and most challenging game yet. I have skipped discussing some things, including the Sky
class and touch handling. I know that by now, these things shouldn't cause you to scratch your head in confusion. So, go have some fun and revel in the satisfaction of what you've accomplished in this chapter!
18.224.51.145