Building the Player-the first class

Let's think about what our Player class will need to do. The class will need to know how fast it can move, where in the game world it currently is, and how much health it has. As the Player class, in the player's eyes, is represented as a 2D graphical character, the class will need both a Sprite and a Texture object.

Furthermore, although the reasons might not be obvious at this point, our Player class will also benefit from knowing a few details about the overall environment the game is running in. These details are screen resolution, the size of the tiles that make up an arena, and the overall size of the current arena.

As the Player class will be taking full responsibility for updating itself each frame, it will need to know the player's intentions at any given moment. For example, is the player currently holding down a particular keyboard direction key? Or is the player currently holding down multiple keyboard direction keys? Boolean variables to determine the status of the W, A, S, and D keys will be essential.

It is plain we are going to need quite a selection of variables in our new class. Having learned all we have about OOP, we will, of course, be making all these variables private. This means that we must provide access, where appropriate, from the main function.

We will use a whole bunch of getter functions, as well as some other functions, to set up our object. These functions are quite numerous; there are actually 21 functions in this class. At first this might seem a little daunting, but we will go through them all and see that the majority of them simply set or get one of the private variables.

There are just a few fairly in-depth functions, such as update, which will be called once each frame from the main function, and spawn, which will handle the initializing of some of the private variables. As we will see, however, there is nothing complicated about them, and they will all be described in detail.

The best way to proceed is to code the header file. This will give us the opportunity to see all the private variables and examine all the function signatures. Pay close attention to the return values and argument types, as this will make understanding the code in the function definitions much easier.

Coding the Player class header file

Right click on Header Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) Header File (.h), and then, in the Name field, type Player.h. Finally, click the Add button. We are now ready to code the header file for our first class.

Get started coding the Player class by adding the declaration, including the opening and closing curly braces followed by a semicolon:

#pragma once 
#include <SFML/Graphics.hpp> 
 
using namespace sf; 
 
class Player 
{ 
 
}; 

Now let's add all our private member variables. Based on what we have already discussed, see if you can work out what each of them will do. We will go through them individually in a minute:

class Player 
{ 
private:   
  const float START_SPEED = 200;   
  const float START_HEALTH = 100; 
  
  // Where is the player   
  Vector2f m_Position;   

  // Of course we will need a sprite   
  Sprite m_Sprite;   

  // And a texture   
  // !!Watch this space!!   
  Texture m_Texture;   

  // What is the screen resolution   
  Vector2f m_Resolution;   

  // What size is the current arena   
  IntRect m_Arena;   

  // How big is each tile of the arena   
  int m_TileSize;   

  // Which directions is the player currently moving in   
  bool m_UpPressed;   
  bool m_DownPressed;   
  bool m_LeftPressed;   
  bool m_RightPressed;   

  // How much health has the player got?   
  int m_Health;   

  // What is the maximum health the player can have   
  int m_MaxHealth;   

  // When was the player last hit   
  Time m_LastHit;   

  // Speed in pixels per second   
  float m_Speed;

  // All our public functions will come next 
}; 

The previous code declares all our member variables. Some are regular variables and some are themselves objects. Notice that they are all under the private: section of the class and are therefore not directly accessible from outside the class.

Also notice, we are using the naming convention of prefixing m_ to all the names of the non-constant variables. The m_ prefix will remind us, while coding the function definitions, that they are member variables and are distinct from some local variables we will create in some of the functions, as well as being distinct from the function parameters.

All of the variable's uses will be obvious, such as m_Position, m_Texture, and m_Sprite, which are for the current location, the texture, and sprite of the player. In addition, each variable (or group of variables) is commented to make their usage plain.

However, why exactly they are needed and the context they will be used in might not be so obvious. For example, m_LastHit, which is an object of type Time, is for recording the time that the player last received a hit from a zombie. The use we are putting m_LastHit to is plain, but at the same time, it is not obvious why we might need this information.

As we piece the rest of the game together, the context for each of the variables will become clearer. The important thing for now is to familiarize yourself with the names and types to make following along with the rest of the project trouble free.

Tip

You don't need to memorize the variable names and types as we will discuss all the code when they are used. You do need to take your time to look over them and get a little bit familiar with them. Furthermore, as we proceed it might be worth referring back to this header file if anything seems unclear.

Now we can add a whole long list of functions. Add all of the following highlighted code and see if you can work out what it all does. Pay close attention to the return types, parameters and name of each function. This is key to understanding the code we will write throughout the rest of the project. What do they tell us about each function? Add the following highlighted code and then we will examine it:

// All our public functions will come next 
public:   
  Player();
  void spawn(IntRect arena, Vector2f resolution, int tileSize);   

  // Call this at the end of every game   
  void resetPlayerStats();   

  // Handle the player getting hit by a zombie   
  bool hit(Time timeHit);   

  // How long ago was the player last hit   
  Time getLastHitTime();   

  // Where is the player   
  FloatRect getPosition();   

  // Where is the center of the player   
  Vector2f getCenter();   

  // Which angle is the player facing   
  float getRotation();   

  // Send a copy of the sprite to main   
  Sprite getSprite();   

  // The next four functions move the player   
  void moveLeft();   

  void moveRight();   

  void moveUp();   

  void moveDown();   

  // Stop the player moving in a specific direction   
  void stopLeft();   

  void stopRight();   

  void stopUp();   

  void stopDown();   

  // We will call this function once every frame   
  void update(float elapsedTime, Vector2i mousePosition);   

  // Give player a speed boost   
  void upgradeSpeed();   

  // Give the player some health   
  void upgradeHealth();   

  // Increase the maximum amount of health the player can have   
  void increaseHealthLevel(int amount);   

  // How much health has the player currently got?   
  int getHealth(); 
}; 

First note, that all the functions are public. This means we can call all of these functions, using an instance of the class, from main, with code like this: player.getSprite();.

Assuming player is a fully set up instance of the Player class, the previous code will return a copy of m_Sprite. Putting this code into a real context, we could, in the main function, write code like this:

window.draw(player.getSprite()); 

The previous code would draw the player graphic in its correct location, just as if the sprite were declared in the main function itself. This is just like what we did with the hypothetical Paddle class previously.

Before we move on to implement (write the definitions of) these functions in a corresponding .cpp file, let's take a closer look at each of them in turn:

  • void spawn(IntRect arena, Vector2f resolution, int tileSize): This function does as the name suggests. It will prepare the object ready for use, including putting it in its starting location (spawning it). Notice that it doesn't return any data, but it does have three arguments. It receives an IntRect called arena, which will be the size and location of the current level, a Vector2f that will contain the screen resolution, and an int which will hold the size of a background tile.
  • void resetPlayerStats: Once we give the player the ability to level up between waves, we will need to be able to take away and reset those abilities when they die.
  • Time getLastHitTime(): This function does just one thing, it returns the time when the player was last hit by a zombie. We will use this function when detecting collisions and it will enable us to make sure the player isn't punished too frequently for contact with a zombie.
  • FloatRect getPosition(): This function returns a FloatRect that describes the horizontal and vertical floating point coordinates of the rectangle which contains the player graphic. This again is useful for collision detection.
  • Vector2f getCenter(): This is slightly different to getPosition because it is a Vector2f and contains just the X and Y locations of the very center of the player graphic.
  • float getRotation(): The code in main will sometimes need to know, in degrees, which way the player is currently facing. Three o'clock is zero degrees and increases clockwise.
  • Sprite getSprite(): As previously discussed, this function returns a copy of the sprite which represents the player.
  • void moveLeft(), ...Right(), ...Up(), ...Down(): These four functions have no return type or parameters. They will be called from the main function and the Player class will then be able to take action when one or more of the W, A, S, and D keys have been pressed.
  • void stopLeft(), ...Right(), ...Up(), ...Down(): These four functions have no return type or parameters. They will be called from the main function, and the Player class will then be able to take action when one or more of the  W, A, S, and D keys have been released.
  • void update(float elapsedTime, Vector2i mousePosition): This will be the only relatively long function of the entire class. It will be called once per frame from main. It will do everything necessary to make sure the player object's data is updated ready for collision detection and drawing. Notice it returns no data, but receives the amount of elapsed time since the last frame, along with a Vector2i, which will hold the horizontal and vertical screen location of the mouse pointer or crosshair.

Tip

Note that these are integer screen coordinates, distinct from floating point world coordinates.

  • void upgradeSpeed(): A function that can be called from the leveling-up screen when the player chooses to make the player faster.
  • void upgradeHealth(): Another function that can be called from the leveling-up screen when the player chooses to make the player stronger (have more health).
  • void increaseHealthLevel(int amount): A subtle but important difference to the previous function in that this one will increase the amount of health the player has, up to a maximum currently set. This function will be used when the player picks up a health pick-up.
  • int getHealth(): With the level of health being as dynamic as it is, we need to be able to determine how much health the player has at any given moment. This function returns an int which holds that value. As with the variables, it should now be plain what each of the functions is for. Also, as with the variables, the why and precise context of using some of these functions will only reveal itself as we progress with the project.

Tip

You don't need to memorize the function names, return types, or parameters as we will discuss all the code when they are used. You do need to take your time to look over them, along with the previous explanations, and get a little bit more familiar with them. Furthermore, as we proceed, it might be worth referring back to this header file if anything seems unclear.

Now we can move on to the meat of our functions, the definitions.

Coding the Player class function definitions

At last we can begin to write the code which actually does the work of our class.

Right click on Source Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) C++ File ( .cpp ), and then in the Name field type Player.cpp. Finally, click the Add button. We are now ready to code the .cpp file for our first class.

Here are the necessary include directives followed by the definition of the constructor. Remember, the constructor will be called when we first instantiate an object of type Player. Add this code into the Player.cpp file and then we can take a closer look:

#include "stdafx.h" 
#include "player.h" 
 
Player::Player() 
{ 
   m_Speed = START_SPEED; 
   m_Health = START_HEALTH; 
   m_MaxHealth = START_HEALTH; 
 
   // Associate a texture with the sprite 
   m_Texture.loadFromFile("graphics/player.png"); 
   m_Sprite.setTexture(m_Texture); 
 
   // Set the origin of the sprite to the centre,  
   // for smooth rotation 
   m_Sprite.setOrigin(25, 25); 
} 

In the constructor function which, of course, has the same name as the class and no return type, we write code which begins to set up the Player object ready for use.

To be absolutely clear: This code will run when we write this code from the main function

Player player;

Don't add this previous line of code just yet.

All we do is initialize m_Speed, m_Health, and m_MaxHealth from their related constants. Then we load the player graphic in to m_Texture, associate m_Texture with m_Sprite, and set the origin of m_Sprite to the center (25, 25).

Tip

Note the cryptic comment // !!Watch this space!!, indicating that we will return to the loading of our texture and some important issues regarding it. We will eventually change how we deal with this texture once we have discovered a problem and learned a bit more C++. We will do so in Chapter 8, Pointers, Standard Template Library, and Texture Management.

Next, we will code the spawn function. We will only ever create one instance of the Player class. We will, however, need to spawn it into the current level, each and every wave. This is what the spawn function will handle for us. Add the following code into the Player.cpp file. Be sure to examine the detail and read the comments:

void Player::spawn(IntRect arena, Vector2f resolution, int tileSize) 
{ 
   // Place the player in the middle of the arena 
   m_Position.x = arena.width / 2; 
   m_Position.y = arena.height / 2; 
 
   // Copy the details of the arena to the player's m_Arena 
   m_Arena.left = arena.left; 
   m_Arena.width = arena.width; 
   m_Arena.top = arena.top; 
   m_Arena.height = arena.height; 
 
   // Remember how big the tiles are in this arena 
   m_TileSize = tileSize; 
 
   // Store the resolution for future use 
   m_Resolution.x = resolution.x; 
   m_Resolution.y = resolution.y; 
 
} 

The previous code starts off by initializing the m_Position.x and m_Position.y values to half the height and width of the passed in arena. This has the effect of moving the player to the center of the level, regardless of its size.

Next, we copy all of the coordinates and dimensions of the passed in arena to the member object of the same type, m_Arena. The details of the size and coordinates of the current arena are used so frequently that it makes sense to do so. We can now use m_Arena for tasks such as making sure the player can't walk through walls. In addition, we copy the passed in tileSize to the member variable m_TileSize, for the same purpose. We will see m_Arena and m_TileSize in action in the update function.

The final two lines of code copy the screen resolution from the Vector2f, resolution, which is a parameter of spawn, into m_Resolution, which is a member variable of Player.

Now add the very straightforward code of the resetPlayerStats function. When the player dies, we will use it to reset any upgrades they might have used:

void Player::resetPlayerStats() 
{ 
   m_Speed = START_SPEED; 
   m_Health = START_HEALTH; 
   m_MaxHealth = START_HEALTH; 
} 

We will not write the code that actually calls the resetPlayerStats function until we have nearly completed the project, but it is there ready for when we need it.

In the next code, we will add two more functions. They will handle what happens when the player is hit by a zombie. We will be able to call player.hit() and pass in the current game time. We will also be able to query the last time that the player was hit by calling player.getLastHitTime(). Exactly how these functions will be useful will become apparent when we have some zombies!

Add the two new functions into the Player.cpp file and then we will examine the C++ a little more closely:

Time Player::getLastHitTime() 
{ 
   return m_LastHit; 
} 
 
bool Player::hit(Time timeHit) 
{ 
   if (timeHit.asMilliseconds() - m_LastHit.asMilliseconds() > 200) 
   { 
      m_LastHit = timeHit; 
      m_Health -= 10; 
      return true; 
   } 
   else 
   { 
      return false; 
   } 
 
} 

The code for getLastHitTime is very straightforward. Return whatever value is stored in m_LastHit.

The hit function is a bit more in-depth and nuanced. First, the if statement checks to see whether the time passed in is 200 milliseconds further ahead than the time stored in m_LastHit. If it is, m_LastHit is updated with the time passed in and m_Health has 10 deducted from its current value. The last line of code in this if statement is return true. Note that the else clause simply returns false to the calling code.

The overall effect of this function is that health will only be deducted from the player up to five times per second. Remember that our game loop might be running at thousands of iterations per second. In this scenario, without the restriction, a zombie would only need to be in contact with the player for one second and tens of thousands of health points would be deducted. The hit function controls and restricts this occurrence. It also lets the calling code know if a new hit has been registered (or not) by returning true or false.

This code implies that we will detect collisions between a zombie and the player in the main function. We will then call player.hit() to determine whether to deduct any health points.

Next, for the Player class we will implement a bunch of getter functions. They enable us to keep the data neatly encapsulated in the Player class, at the same time as making their values available to the main function.

Add the following code right after the previous block and then we will discuss exactly what each function does:

FloatRect Player::getPosition() 
{ 
   return m_Sprite.getGlobalBounds(); 
} 
 
Vector2f Player::getCenter() 
{ 
   return m_Position; 
} 
 
float Player::getRotation() 
{ 
   return m_Sprite.getRotation(); 
} 
 
Sprite Player::getSprite() 
{ 
   return m_Sprite; 
} 
 
int Player::getHealth() 
{ 
   return m_Health; 
} 

The previous code is very straightforward. Each and every one of the previous five functions returns the value of one of our member variables. Look carefully at each and familiarize yourself with which function returns which value.

The next eight short functions enable the keyboard controls (we will use from main) to change data contained in our object of type Player. Add the code in the Player.cpp file and then I will summarize how it all works:

void Player::moveLeft() 
{ 
   m_LeftPressed = true; 
} 
 
void Player::moveRight() 
{ 
   m_RightPressed = true; 
} 
 
void Player::moveUp() 
{ 
   m_UpPressed = true; 
} 
 
void Player::moveDown() 
{ 
   m_DownPressed = true; 
} 
 
void Player::stopLeft() 
{ 
   m_LeftPressed = false; 
} 
 
void Player::stopRight() 
{ 
   m_RightPressed = false; 
} 
 
void Player::stopUp() 
{ 
   m_UpPressed = false; 
} 
 
void Player::stopDown() 
{ 
   m_DownPressed = false; 
} 

The previous code has four functions (moveLeft, moveRight, moveUp, moveDown) which set the related Boolean variables (m_LeftPressed, m_RightPressed, m_UpPressed, m_DownPressed) to true. The other four functions (stopLeft, stopRight, stopUp, stopDown) do the opposite and set the same Boolean variables to false. The instance of the Player class can now be kept informed of which of the  W, A, S, and D keys have been pressed and which are not.

This next function is the one which does all the hard work. The update function will be called once on every single frame of our game loop. Add the code that follows and we will then examine it in detail. If you followed along with the previous eight functions and you remember how we animated the clouds for the Timber!!! project, you will probably find most of the following code quite understandable:

void Player::update(float elapsedTime, Vector2i mousePosition) 
{ 
   if (m_UpPressed) 
   { 
      m_Position.y -= m_Speed * elapsedTime; 
   } 
 
   if (m_DownPressed) 
   { 
      m_Position.y += m_Speed * elapsedTime; 
   } 
 
   if (m_RightPressed) 
   { 
      m_Position.x += m_Speed * elapsedTime; 
   } 
 
   if (m_LeftPressed) 
   { 
      m_Position.x -= m_Speed * elapsedTime; 
   } 
 
   m_Sprite.setPosition(m_Position); 
 
   // Keep the player in the arena 
   if (m_Position.x > m_Arena.width - m_TileSize) 
   { 
      m_Position.x = m_Arena.width - m_TileSize; 
   } 
 
   if (m_Position.x < m_Arena.left + m_TileSize) 
   { 
      m_Position.x = m_Arena.left + m_TileSize; 
   } 
 
   if (m_Position.y > m_Arena.height - m_TileSize) 
   { 
      m_Position.y = m_Arena.height - m_TileSize; 
   } 
 
   if (m_Position.y < m_Arena.top + m_TileSize) 
   { 
      m_Position.y = m_Arena.top + m_TileSize; 
   } 
 
   // Calculate the angle the player is facing 
   float angle = (atan2(mousePosition.y - m_Resolution.y / 2, 
      mousePosition.x - m_Resolution.x / 2) 
      * 180) / 3.141; 
 
   m_Sprite.setRotation(angle); 
} 

The first portion of the previous code moves the player sprite. The four if statements check which of the movement-related Boolean variables (m_LeftPressed, m_RightPressed, m_UpPressed, m_DownPressed) are true and changes m_Position.x and m_Position.y accordingly. The same formula to calculate the amount to move as the Timber!!! project is used.

position (+ or -) speed * elapsed time.

After these four if statements, m_Sprite.setPosition is called and m_Position is passed in. The sprite has now been adjusted by exactly the right amount for that one frame.

The next four if statements check whether m_Position.x or m_Position.y are beyond any of the edges of the current arena. Remember that the confines of the current arena were stored in m_Arena in the spawn function. Let's look at the first of these four if statements in order to understand them all:

if (m_Position.x > m_Arena.width - m_TileSize) 
{ 
   m_Position.x = m_Arena.width - m_TileSize; 
} 

The previous code tests to see if m_position.x is greater than m_Arena.width minus the size of a tile (m_TileSize). As we will see when we create the background graphics, this calculation will detect the player straying into the wall.

When the if statement is true, the calculation m_Arena.width - m_TileSize is used to initialize m_Position.x. This makes the center of the player graphic unable to stray past the left-hand edge of the right-hand wall.

The next three if statements that follow the one we have just discussed do the same thing for the other three walls.

The last two lines of code calculate and set the angle that the player sprite is rotated to (facing). The line of code might look a little complex, but it is simply using the position of the crosshair (mousePosition.x and mousePosition.y) and the center of the screen (m_Resolution.x and m_Resolution.y) in a tried and tested trigonometric function.

How atan uses these coordinates along with Pi (3.141) is quite complicated, and that is why it is wrapped up in a handy function for us. If you want to explore trigonometric functions in more detail you can do so at http://www.cplusplus.com/reference/cmath/.The last three functions for the Player class make the player 20% faster, have 20% more health, and increase the player's health by the amount passed in, respectively.

Add this code at the end of the Player.cpp file and then we will take a closer look:

void Player::upgradeSpeed() 
{ 
   // 20% speed upgrade 
   m_Speed += (START_SPEED * .2); 
} 
 
void Player::upgradeHealth() 
{ 
   // 20% max health upgrade 
   m_MaxHealth += (START_HEALTH * .2); 
 
} 
 
void Player::increaseHealthLevel(int amount) 
{ 
   m_Health += amount; 
 
   // But not beyond the maximum 
   if (m_Health > m_MaxHealth) 
   { 
      m_Health = m_MaxHealth; 
   } 

}

In the previous code, the upgradeSpeed and upgradeHealth functions increase the values stored in m_Speed and m_MaxHealth, respectively. The values are increased by 20% by multiplying the starting values by 0.2 and adding them to the current values. These functions will be called from the main function when the player is choosing what attributes of their character they wish to improve between levels.

The increaseHealthLevel takes an int value from main in the amount parameter. This int value will be provided by a class called Pickup that we will write in Chapter 9, Collision Detection, Pick-ups, and Bullets. The m_Health member variable is increased by the passed in value. There is a catch for the player, however. The if statement checks whether m_Health has exceeded m_MaxHealth, and if it has, sets it to m_MaxHealth. This means the player cannot simply gain infinite health from pick-ups. They must instead carefully balance the upgrades they choose between levels.

Of course, our Player class can't actually do anything until we instantiate it and put it to work in our game loop. Before we do that, let's take a look at the concept of a game camera.

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

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