Chapter 2. Creating a Playable Character

In any game, the user will be interacting with and experience the world in which they inhabit. In order to achieve this, we need some way for us to interact with the game world. Like other games, CRYENGINE games also use something called a player to accomplish this. It acts as a sort of proxy or interface for the user to interact with and experience the world. In this chapter, you will learn how to create such a player. Here are the topics we will discuss:

  • Implementing player lives
  • Implementing player movement and rotation
  • Making a camera follow the player

Implementing player lives

In games, there is often a need to have a mechanism that allows the user multiple attempts to accomplish a certain task. This concept is called lives, and our Great Action Side-Scroller, or GAS, game will use this mechanism. When we talk about player lives, we cannot avoid talking about player health, the mechanism for determining when a player's life has been exhausted. When health reaches zero, we die and another life will be consumed. Together, these two concepts form all that's needed to implement player lives. In this section, we will learn how to implement player lives. This is a two-step process which includes two subsections. Let's get started.

Part 1 – the player code

In this subsection, we will implement all the player-side code needed to implement the player lives gameplay-mechanic. Going forward, any class that is referenced can be assumed to be located in a file of the same name, for example, the CPlayer class declaration would be located in the CPlayer.h header file, and its definition would be inside the CPlayer.cpp source file. All the code used in this book should completely replace any default starter-kit code, unless otherwise instructed. So, let's dive in without further ado:

  1. Add an m_fHealth private member variable of type float to the CPlayer class to store and keep track of the current player health. It should look like this:
    private:float m_fHealth;
  2. Add an m_iLives private member variable of type int to the CPlayer class to store and keep track of the amount of lives the player has left. It should look like this:
    private:int m_iLives;
  3. Add and implement public getter methods to the CPlayer class so that we can allow the rest of CRYENGINE to query our player's current health and lives. This is important as our class doesn't operate in a vacuum; more specifically, our Game Rules class should be able to query the player status in order to make gameplay decisions on what to do when we run out of health or exceed our maximum lives limit. We will not add/implement setter methods because we don't want to allow external code to set our health and lives variables. Instead, we want to exclusively use our damage/revive mechanism (ApplyDamage() or Revive()). It should look like this:
    float CPlayer::GetHealth()const
    {
      return m_fHealth;
    }
    
    float CPlayer::GetMaxHealth()const
    {
      return 100;
    }
    
    unsigned int CPlayer::GetLives()const
    {
      return m_iLives;
    }
  4. Add and implement a public ApplyDamage() method in the CPlayer class so that our player can take damage and dispatch when its health has been depleted. It should look like this:
    void CPlayer::ApplyDamage( float fDamage )
    {
      //Apply Damage
      m_fHealth -= fDamage;
    
      //If Health Is Less Than Or Equal To 0 Then Dispatch That Our Health Has Been Depleted.  Also Prevent Health From Being Negative.
      if( m_fHealth <= 0.0f )
      {
        //Avoid A Negative Health Value.
        m_fHealth = 0.0f;
    
        //Notify About Depleted Health.
        OnHealthDepleted();
      }
    }
  5. Add and implement a private OnHealthDepleted() method in the CPlayer class to handle player death. The method will be private so that it can only be called within the class. It should look like this:
    void CPlayer::OnHealthDepleted()
    {
      //Get Our Implementation Of The IGameRules Interface To Notify About The Player's Health Being Depleted. The Game's Rules Should Handle What Happens When The Player's Health Is Depleted.
      auto pGameRules = static_cast< CGASGameRules* > ( gEnv->pGame->GetIGameFramework()->GetIGameRulesSystem()->GetCurrentGameRules() );
    
      //Actually Notify The Game's Rules About Our Player's Health Being Depleted.
      pGameRules->OnPlayerHealthDepleted( this );
    }
  6. Add and implement a public Revive() method in the CPlayer class so that our player can be revived when it has died. It should look like this:
    bool CPlayer::Revive()
    {
      //If We Have Some Lives Left, Then Revive.
      if( m_iLives )
      {
        //Set Our Health Back To Maximum.
        m_fHealth = GetMaxHealth();
    
        //We Were Able To Revive.
        return true;
      }
      else
        return false; //We Have No More Lives Left So We Can't Revive.
    }
  7. Add and implement a public Kill() method in the CPlayer class so that the player can be killed. It should look like this:
    void CPlayer::Kill()
    {
      //Subtract 1 Life, And Prevent Lives From Being Negative.
      if( --m_iLives <= 0 )
        m_iLives = 0;
      
      //Get Our Implementation Of The IGameRules Interface To Notify About The Player Losing A Life. The Game's Rules Should Handle What Happens When The Player Dies.
      auto pGameRules = static_cast< CGASGameRules* > ( gEnv->pGame->GetIGameFramework()->GetIGameRulesSystem()->GetCurrentGameRules() );
    
      //Actually Notify The Game's Rules About Our Player's Life Being Lost.
      pGameRules->OnPlayerDied( this );
    }

Congratulations, we have just implemented all of the player code necessary in order to use the player lives gameplay-mechanic. Now, we will move on to the GameRules side of the code.

Part 2 – the GameRules code

In this subsection, we will implement all the GameRules code needed to implement the player lives gameplay-mechanic. The steps are as follows:

  1. Add and implement a public OnPlayerHealthDepleted() method in the CGASGameRules class so that we can handle the gameplay logic for what happens when the player's health reaches 0. It should look like this:
    void CGASGameRules::OnPlayerHealthDepleted( IActor*const pPlayer )
    {
      //Get Our Implementation Of The IActor Interface.
      auto pCPlayer = static_cast< CPlayer*const >( pPlayer );
    
      //If Pointer Is Valid, Kill Our Player.
      if( pCPlayer )
        pCPlayer->Kill();
    }
  2. Add and implement a public OnPlayerDied() method in the CGASGameRules class so that we can handle the gameplay logic for what happens when the player dies. It should look like this:
    void CGASGameRules::OnPlayerDied( IActor*const pPlayer )
    {
      //Get Our Implementation Of The IActor Interface.
      auto pCPlayer = static_cast< CPlayer*const >( pPlayer );
    
      //If Pointer Is Valid, Try To Revive Our Player.
      if( pCPlayer )
        if( !pCPlayer->Revive() ) //Try To Revive Our Player
          OnAllPlayerLivesLost( pPlayer ); //Failed To Revive Our Player. Notify About The Player Having No More Lives.
    }
  3. Add and implement a private OnAllPlayerLivesLost() method in the CGASGameRules class so that we can handle the gameplay logic for what happens when the player has lost all of its lives. This method is private to avoid other classes from calling this method. It should look like this:
    void CGASGameRules::OnAllPlayerLivesLost( IActor*const pPlayer )
    {
      //Should Probably Trigger End Of Game UI Menu.
    }

Congratulations, we have just implemented all of the GameRules code necessary to use the player lives gameplay-mechanic.

The big picture

Let's look at the big picture so that we can get a clear view of the full flow and geography of the code. Externally (external from the CPlayer class), we will only be calling a couple of CPlayer methods, such as ApplyDamage(), Kill(), and Revive(). When the ApplyDamage() method is called, the player takes damage and checks if its health is depleted. If the player's health is depleted, we notify our GameRules implementation that the player has no more health by calling the GameRules' OnPlayerHealthDepleted() method. The GameRules then kills the player by calling the player's Kill() method, which subtracts one life and dispatches back to the GameRules that the player has died by calling the GameRules' OnPlayerDied() method. The GameRules then tries to revive the player by calling the player's Revive() method. If reviving the player fails, the GameRules will call its own OnAllPlayerLivesLost() method, which handles the game over gameplay logic. To help visualize this, I have created a simple flowchart explaining the system:

The big picture

It may seem odd at first that we are seemingly and needlessly going back and forth between the CPlayer and CGASGameRules classes. Surely we could do without the communication and just implement the kill and revive logic right inside the CPlayer class? Yes, you could do that, but doing it the way we have offers one clear advantage: modularity. In game development (or in programming in general), it is very important to keep systems separate and modular, in order to have distinct roles between classes. The CPlayer class handles the player logic and the CGASGameRules class handles the game's rules. No direct communication exists between the two classes/systems. Instead, they communicate through notifications or events. This clear separation of logic allows us to have clean, modular code that can be changed in a vacuum without causing a cascading effect. We are free to change the logic of how the player should die for instance (CPlayer::Kill()). Maybe we want to add in a death animation and other death/kill effects. Using this event-based approach, we are free to add such code and the game would still know how to end the game on death or kill the player because the GameRules code is separate and untouched. This concludes the section on implementing player lives.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.102.249