The Hero
class will inherit from GameObject
and will be responsible for handling the state machine of the hero and collision response, among a few other things. The constructor for the Hero
class looks something like this:
Hero::Hero() { type_ = E_GAME_OBJECT_HERO; state_ = E_HERO_STATE_NONE; is_on_ground_ = false; current_ground_height_ = 0.0f; platform_below_ = NULL; }
We start off by setting the type_
of this object to E_GAME_OBJECT_HERO
. We then initialize the member variables. The state_
variable (like you saw in Chapter 5, Let's Get Physical!) is the key to maintaining the state machine for the hero's behavior. We also have an is_on_ground_
flag that indicates when the hero is standing on a surface, which could be either a brick or a moving platform. So, we can set the appropriate state accordingly. We also have a current_ground_height_
variable that measures the height the hero has managed to reach and is at currently. This increases as the hero moves higher from one platform to another and reduces when he falls down. Lastly, we maintain a reference to a Platform
object.
The various states that the player can have are enumerated by enum EHeroState
in GameGlobals.h
as follows:
enum EHeroState { E_HERO_STATE_NONE = 0, E_HERO_STATE_IDLE, E_HERO_STATE_WALKING, E_HERO_STATE_JUMPING, E_HERO_STATE_SWINGING, E_HERO_STATE_DYING, E_HERO_STATE_WINNING, };
The state machine in this class is identical to the Clown
class you saw in Chapter 5, Let's Get Physical!. So, I will skip going over the details of the SetState
function and each subsequent state function. We saw how the collision detection works in the previous sections and left the collision response for later. Now, it is time to define how the hero will respond to collisions. So, let's define the CollisionResponse
function in Hero.cpp
:
void Hero::CollisionResponse(int tile_col, int tile_row, ECollisionType collision_type) { switch(collision_type) { case E_COLLISION_TOP: // stop moving speed_.y = 0; // collision occured above AABB...reposition the AABB one pixel below the collided tile aabb_.origin.y = GET_Y_FOR_ROW(tile_row + 1, game_world_->GetRows()) - aabb_.size.height - 1; // tell the game world to remove this brick game_world_->RemoveBrick(tile_col, tile_row); break; case E_COLLISION_BOTTOM: // stop moving speed_.y = 0; // collision occured below AABB...reposition the AABB one pixel above the collided tile aabb_.origin.y = GET_Y_FOR_ROW( tile_row, game_world_->GetRows()) + 1; // hero has landed on a brick SetIsOnGround(true); break; case E_COLLISION_LEFT: // stop moving speed_.x = 0; // collision occured to the left AABB...reposition the AABB one pixel after the collided tile aabb_.origin.x = GET_X_FOR_COL(tile_col + 1) + 1; break; case E_COLLISION_RIGHT: // stop moving speed_.x = 0; // collision occured to the right AABB...reposition the AABB one pixel before the collided tile aabb_.origin.x = GET_X_FOR_COL(tile_col) - aabb_.size.width - 1; break; } }
As you read earlier, the collision detection passes in the column and row of the collision tile, and also the type of collision that was detected. Based on this collision type, we have a little switch case that quite simply handles each type of collision separately. This comes in handy when you'd want to have different animations for when your character hits his head against the ceiling (E_COLLISION_TOP
) and when it hits the chest against the wall (E_COLLISION_RIGHT
).
For each type of collision, the first thing to do is to prevent the hero from continuing in the same state of motion, and that is why we set the speed in the respective direction to 0
. We then proceed to correct the position of the AABB so that it is just one pixel away from the collision tile. This is done irrespective of the type of collision, because we would never want the hero to overlap any of the bricks.
An interesting thing is done for collisions that occur above the hero. We tell GameWorld
to remove the brick at the given column and row. For collisions below the player, we call the SetIsOnGround
function with true
as the parameter.
The hero in Iceman seems like an elderly chap, but don't be fooled because he still has a lot of speed. Let's see how he works so energetically in the UpdateSpeed
function of Hero.cpp
:
void Hero::UpdateSpeed(bool must_go_left, bool must_go_right, bool must_jump) { // add gravity & clamp vertical velocity speed_.y += GRAVITY; speed_.y = (speed_.y < MAX_VELOCITY_Y) ? MAX_VELOCITY_Y : speed_.y; // is the hero above a platform if(platform_below_) { // stop falling due to gravity speed_.y = 0; // move the hero along with the platform he's standing on aabb_.origin.x += platform_below_->GetSpeed().x; aabb_.origin.y = platform_below_->GetAABB().getMaxY(); } // set speed accordingly if the hero must jump if(must_jump && is_on_ground_) { speed_.y = TILE_SIZE * 0.75f; } // increase/decrease the horizontal speed based on the button pressed if(must_go_left) { speed_.x -= HERO_MOVEMENT_FORCE; speed_.x = (speed_.x < -MAX_VELOCITY_X) ? -MAX_VELOCITY_X : speed_.x; setFlipX(true); } if(must_go_right) { speed_.x += HERO_MOVEMENT_FORCE; speed_.x = (speed_.x > MAX_VELOCITY_X) ? MAX_VELOCITY_X : speed_.x; setFlipX(false); } // gradually come to a halt if no button is pressed if(!must_go_left && !must_go_right) { speed_.x -= speed_.x / 5; } // change from idle to walking & vice versa based on horizontal velocity if(fabs(speed_.x) > 0.5f) { SetState(E_HERO_STATE_WALKING); } else if(state_ == E_HERO_STATE_WALKING) { SetState(E_HERO_STATE_IDLE); } }
This function is called from the Update
function of the same class and is passed the flags that indicate whether the player has pressed the left, right, or jump buttons on the screen. We begin this function by first adding the force of gravity to the y component of speed_
and clamp it to a maximum value.
Before we react to the user input flags, we must first check to see whether the hero is standing above a platform. If he is, then most certainly he shouldn't be falling. Also, he should now move with the platform beneath him so we add the platform's horizontal speed to aabb_
. We also set the y component of the aabb_
to be right above the platform.
With that out of the way, we can react to the respective user input flags. If jump is pressed, we simply add a predefined value to the y component of speed_
, which will send the hero shooting upwards. Similarly, if the left or right buttons are pressed, we subtract or add a predefined force to the x component of speed_
. Finally, we flip the hero so that he is facing the same direction as he is moving towards.
We also have a condition when the player releases the buttons. The hero must now come to a stop and that is exactly what the next condition does. Notice how the x component is gradually reduced; this is to avoid the hero coming to a sudden halt. The last condition checks the x component of speed_
to set the appropriate walking or idle state.
Let's now focus our attention on the Update
function of the Hero.cpp
file:
void Hero::Update(bool must_go_left, bool must_go_right, bool must_jump, bool must_swing) { // let the hero die in peace if(state_ == E_HERO_STATE_DYING) return; // update speed based on user input UpdateSpeed(must_go_left, must_go_right, must_jump); // this enables the hero to leave from the left edge and reappear from the right edge if(aabb_.origin.x < TILE_SIZE) { aabb_.origin.x = SCREEN_SIZE.width - TILE_SIZE - aabb_.size.width; } if(aabb_.origin.x + aabb_.size.width > SCREEN_SIZE.width - TILE_SIZE) { aabb_.origin.x = TILE_SIZE; } // update the AABB GameObject::Update(); // check for collisions game_world_->CheckCollisions(this); game_world_->CheckHeroEnemyCollisions(); game_world_->CheckHeroPlatformCollisions(); setPosition(ccp(aabb_.origin.x + aabb_.size.width * 0.5f, aabb_.origin.y + m_obContentSize.height * 0.5f)); // check if the hero should swing if(must_swing) { SetState(E_HERO_STATE_SWINGING); } else if(state_ == E_HERO_STATE_SWINGING) { SetState(E_HERO_STATE_IDLE); } // check if the hero could be falling if(fabs(speed_.y) > 0.1f) { SetIsOnGround(false); } }
The hero's Update
function is called from the main Update
loop of GameWorld
and is passed four flags that basically indicate which onscreen buttons the player has currently pressed. We begin this function by checking whether the hero is in the dying state and quietly return from there if this condition is met. We then call the UpdateSpeed
function, followed by a conditional that ensures the hero reappears at one end of the screen on exiting from the other end.
We now call the Update
function of GameObject
, which simply updates the aabb_
with the speed_
. With this updated aabb_
, we now call the CheckCollisions
function from GameWorld
, which internally calls the CheckVerticalCollisions
and CheckHorizontalCollisions
functions that you saw earlier. In addition to this, we also ask GameWorld
to check for collisions with the hero and the enemies and the platforms.
Once the collision detection and response has completed, we set the corrected position of the hero. Now, if the user has pressed the swing button, we set the appropriate swinging state. Finally, we check to see if the vertical speed is greater than a minimum value. This indicates that the player is airborne so we call the SetIsOnGround
function, passing in false
as the parameter.
Now that we have the collision response and the Update
functions out of the way, all we have left is the SetIsOnGround
function and we're done coding our zesty old chap.
Let's look at the following code:
void Hero::SetIsOnGround(bool is_on_ground) { /// only accept a change if(is_on_ground_ == is_on_ground) return; is_on_ground_ = is_on_ground; if(is_on_ground_) { // save the height the hero is at currently current_ground_height_ = aabb_.origin.y; SetState(E_HERO_STATE_IDLE); } else { // going up means jumping...for now if(speed_.y > 0) { SetState(E_HERO_STATE_JUMPING); } } }
In the SetIsOnGround
function, we proceed only if there is a change in this information. If the hero has indeed landed on ground, we update the current_ground_height_
variable to whatever height the bottom edge of AABB is at currently before setting the hero to idle. However, if the hero is not on the ground and the y component of speed_
is positive, it means that the hero is jumping and we must set the appropriate state. With that, we are done with class Hero
. Let's now quickly define the Enemy
class.
3.21.46.78