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.
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.
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.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.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.
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)
.
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.
3.128.226.255