The Penguin
class publicly inherits from CCSprite
and it is similar in terms of structure to the GameObject
class you saw in the previous chapter. It possesses a b2Body
object along with functions that update its position based on its body. The body of the penguin will have a circle-shaped fixture applied to it. We will skip all of that boilerplate code and discuss the behavior of the penguin.
Let's now write the functions that define the behavior of the penguin based on the preceding states, starting with the SetIsAwake
function from the Penguin.cpp
file:
void Penguin::SetIsAwake(bool is_awake) { // activate the body & apply impulse in the top-right direction if(is_awake) { is_awake_ = true; body_->SetActive(true); body_->ApplyLinearImpulse(b2Vec2(0.1f, 0.2f), body_->GetWorldCenter()); } // deactivate the body else { is_awake_ = false; body_->SetActive(false); } }
As you can read from the table, the penguin is initially asleep and hence the is_awake_
flag is initialized to false
. However, when the player touches the screen for the first time, this function is called with a value of true
passed in. In that case, we activate the physics body and apply an impulse such that the penguin is pushed in the upright direction. Bodies that are not active have no physics processing; so the ApplyImpulse
function would have been without effect if we had not activated the penguin's body.
Conversely, when SetIsAwake
is called with the false argument, we merely deactivate the physics body. Let's now look at the SetIsDiving
function of Penguin.cpp
:
void Penguin::SetIsDiving(bool is_diving) { if(is_diving_ == is_diving) return; is_diving_ = is_diving; // set sprite for the respective state if(is_diving_) { initWithFile("penguin_3.png"); } else { initWithFile("penguin_2.png"); } }
This function just toggles the is_diving_
flag and resets the sprite for the penguin. However, the real magic happens in the update loop, but more on that later. For now, we'll proceed to the SetIsFlying
function of Penguin.cpp
:
void Penguin::SetIsFlying(bool is_flying) { if(is_flying_ == is_flying) return; is_flying_ = is_flying; // if penguin has taken off, check for a perfect slide if(is_flying_ && !is_leaping_) { // if take off velocity is above certain threshold, its a perfect slide if(body_->GetLinearVelocity().Length() >= PERFECT_TAKE_OFF_VELOCITY) { OnPerfectSlide(); } } }
This function is called every tick and the parameter passed in, and it informs us whether the penguin is currently on the ground or airborne based on whether or not a contact exists between the penguin and the terrain.
The sole purpose of this function is to determine whether the penguin has managed to pull off a perfect slide. To do that, we simply check what the magnitude of the velocity vector is when the penguin enters the flying stage, that is, when it has just taken off. If this value is greater than a predetermined threshold (PERFECT_TAKE_OFF_VELOCITY
), we can safely say that the slide was carried out with perfection.
Let's now define the way in which we can reward the player for a perfect slide in the OnPerfectSlide
function of Penguin.cpp
:
void Penguin::OnPerfectSlide() { // increment counters ++ num_perfect_slides_; ++ total_perfect_slides_; // 3 slides in a row activate fever mode if(num_perfect_slides_ == 3) { StartFever(); } // a perfect slide deserves some extra time int time = 3 * (is_feverish_ ? 2 : 1); game_world_->AddTime(time); // inform player of the perfect slide char buf[16] = {0}; sprintf(buf, "Perfect +%ds", time); game_world_->ShowMessage(buf); }
At the start of the function, we increment the num_perfect_slides_
and total_perfect_slides_
counters that stand for the number of consecutive perfect slides and the total number of perfect slides respectively. The next if
condition simply turns the fever mode on. The fever mode is visually represented by a trail of particles behind the penguin.
The code after that deals with adding time to the gameplay. Where did this concept of time-keeping come from? In this game, players have a limited amount of time (in seconds) to get the penguin as far as they can. The only way to increase this playing time is to perform perfect slides and to leap off from one hill to the next. On a perfect slide, players are rewarded 3 seconds and on leaping off from one hill to the next, players are rewarded 5 seconds. The best thing about fever mode is that the player is awarded with double time as long as the penguin is in fever mode.
The last bit of code calls the ShowMessage
function from the GameWorld
class that accepts a char array as input and displays the text within the char array with some gratuitous animations for entry and exit. We're almost done with the behavior of our little penguin, except for leaping, which we will define in the StartLeap
and FinishLeap
functions of Penguin.cpp
:
void Penguin::StartLeap() { if(is_leaping_) return; // successfully conquered a hill...reward player with extra time is_leaping_ = true; game_world_->AddTime(5 * (is_feverish_ ? 2 : 1)); // no gravity so the penguin can be transported weightlessly to the next hill body_->SetGravityScale(0.0f); } void Penguin::FinishLeap() { if(!is_leaping_) return; is_leaping_ = false; // next hill has arrived, turn gravity back on body_->SetGravityScale(1.0f); is_diving_ = false; }
In the StartLeap
function, we simply tell GameWorld
to award the player 5 seconds of time and to double that if the penguin is feverish. Along with that, we also set the gravity scale on the penguin's body to 0
. If you remember from the previous chapter, this simply means that the said physics body will not be affected by gravity. The reason why we're doing this here is so that we can effortlessly lift the penguin high up so that the player is ready to take on the next hill. You will see that code in action when we define the updating functions of the Penguin
class. Conversely, in the FinishLeap
function, we finish a leap when the penguin is close enough to the next set of hills. Hence, we set the gravity scale to its default value.
Now that we've defined most of the penguin's behavior, it's time to tie it all together with the update
logic. We will split the update logic into two main methods: UpdatePhysics
and UpdateNode
. Here is the code for the UpdatePhysics
function of Penguin.cpp
:
void Penguin::UpdatePhysics() { if(is_diving_) { // if penguin is asleep, awaken him and cancel the dive if(!is_awake_) { SetIsAwake(true); is_diving_ = false; } // else apply a downward force provided penguin isn't leaping else if(!is_leaping_) { body_->ApplyForceToCenter(b2Vec2(0.5f, -2.0f)); } } // restrict velocity between minimum & maximum b2Vec2 vel = body_->GetLinearVelocity(); vel.x = vel.x < MIN_PENGUIN_VELOCITY_X ? MIN_PENGUIN_VELOCITY_X : (vel.x > MAX_PENGUIN_VELOCITY_X ? MAX_PENGUIN_VELOCITY_X : vel.x); vel.y = vel.y < MIN_PENGUIN_VELOCITY_Y ? MIN_PENGUIN_VELOCITY_Y : (vel.y > MAX_PENGUIN_VELOCITY_Y ? MAX_PENGUIN_VELOCITY_Y : vel.y); body_->SetLinearVelocity(vel); UpdateLeap(); }
This function begins by checking if the player wants the penguin to dive. Remember that this flag is set in the SetIsDiving
function when the player taps the screen. Within this condition, we must first check if the penguin is awake or not. At the start of the game, the penguin is asleep, so we wake him up and reset the is_diving_
flag. In the else if
condition, we check if the penguin is not currently leaping. Then, we apply a downward force causing the penguin to lunge towards the hill.
Outside the conditional, we ensure that the velocity of the penguin stays within the limits defined by the four variables, that is, MIN_PENGUIN_VELOCITY_X
, MIN_PENGUIN_VELOCITY_Y
, MAX_PENGUIN_VELOCITY_X
, and MAX_PENGUIN_VELOCITY_Y
. You can find these variables defined in GameGlobals.h
.
To finish off this function, we make a call to the UpdateLeap
function of Penguin.cpp
:
void Penguin::UpdateLeap() { if(!is_leaping_) return; b2Vec2 vel = body_->GetLinearVelocity(); b2Vec2 new_vel = vel; // increase the velocity new_vel.x += (MAX_PENGUIN_VELOCITY_X - new_vel.x) / 15.0f; new_vel.y += (MAX_PENGUIN_VELOCITY_Y - new_vel.y) / 15.0f; // ensure velocity doesn't exceed maximum new_vel.x = (new_vel.x > MAX_PENGUIN_VELOCITY_X ) ? MAX_PENGUIN_VELOCITY_X : new_vel.x; new_vel.y = (new_vel.y > MAX_PENGUIN_VELOCITY_Y ) ? MAX_PENGUIN_VELOCITY_Y : new_vel.y; body_->SetLinearVelocity(new_vel); }
The UpdateLeap
function must ensure that the penguin moves from one hill to the next. To do this, we increase the x and y components of the penguin's linear velocity every time this function is called. We also ensure that we stay within the velocity limits. This will simply cause the penguin to soar high above the hills, thereby ensuring it doesn't fall off the cliff and into the terrible abyss. Why don't you see what's below the hills?
We will now discuss the UpdateNode
function of Penguin.cpp
:
void Penguin::UpdateNode() { // set node position based on body CCPoint previous_position = m_obPosition; setPositionX(WORLD_TO_SCREEN(body_->GetPosition().x)); setPositionY(WORLD_TO_SCREEN(body_->GetPosition().y)); // set rotation based on body float angle = CC_RADIANS_TO_DEGREES(-1 * ccpToAngle( ccpSub(m_obPosition, previous_position))); setRotation(angle); // fetch list of contacts b2Contact* contact = game_world_->GetWorld()->GetContactList(); // if contact exists, penguin has landed if(contact) { SetIsFlying(false); } // else penguin is airborne else { SetIsFlying(true); } // update the trail if penguin is feverish if(is_feverish_) { trail_->setPosition(m_obPosition); } }
You must recognize the first steps within the function where we simply set the position and rotation for the penguin's node according to its body. What follows next is another way to check for collisions, where we simply iterate over the list of the contacts that the Box2D world has recorded. Since we have just one dynamic body (the penguin) and one static body (the hill), it is safe for us to assume that the penguin has landed on the hill whenever we find a contact in the list.
Now, I'm sure you're wondering what's wrong with using contact listeners like we did in the previous chapter. The BeginContact
and EndContact
callbacks are called very frequently, owing to the curved shape of the surface of the hill. To see exactly what I mean, you should implement the BeginContact
and EndContact
functions in GameWorld.cpp
. Put in a call to the CCLOG
function in both the callbacks to see just how often they're fired.
Once we've detected a collision, we react to it by calling the SetIsFlying
function. The last bit of code there is to reset the position of the trail so long as the penguin is feverish. At this point, we have defined the two major entities of the game: the Terrain
class and the Penguin
class.
3.133.122.68