Now we know the basics about inheritance, polymorphism, and pure virtual functions, we will put them to use. We will build a PlayableCharacter
class that has the vast majority of the functionality that any character from our game is going to need. It will have one pure virtual function, handleInput
. The handleInput
function will need to be quite different in the sub-classes, so this makes sense.
As PlayableCharacter
will have a pure virtual function, it will be an abstract class and no objects of it will be possible. We will then build both Thomas
and Bob
classes, which will inherit from PlayableCharacter
, implement the definition of the pure virtual function, and allow us to instantiate Bob
and Thomas
objects in our game.
As usual, when creating a class, we will start off with the header file that will contain the member variables and function declarations. What is new is that in this class, we will declare some protected member variables. Remember that protected variables can be used as if they are Public
by classes, which inherit from the class with the protected variables.
Right-click 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 PlayableCharacter.h
. Finally, click the Add button. We are now ready to code the header file for the PlayableCharacter
class.
We will add and discuss the contents of the PlayableCharacter.h
file in three sections. First, the protected section, followed by private, then public.
Add the code shown next to the PlayableCharacter.h
file:
#pragma once #include <SFML/Graphics.hpp> using namespace sf; class PlayableCharacter { protected: // Of course we will need a sprite Sprite m_Sprite; // How long does a jump last float m_JumpDuration; // Is character currently jumping or falling bool m_IsJumping; bool m_IsFalling; // Which directions is the character currently moving in bool m_LeftPressed; bool m_RightPressed; // How long has this jump lasted so far float m_TimeThisJump; // Has the player just initialted a jump bool m_JustJumped = false; // Private variables and functions come next
The first thing to notice in the code we just wrote is that all the variables are protected
. This means that when we extend the class, all the variables we just wrote will be accessible to those classes that extend it. We will extend this class with Thomas
and Bob
classes.
Apart from the protected
access specification, there is nothing new or complicated about the previous code. It is worth paying attention to some of the details, however. Then it will be easy to understand how the class works as we progress. So, let's run through those protected
variables, one at a time.
We have our somewhat predictable Sprite
, m_Sprite
. We have a float called m_JumpDuration
, which will hold a value representing the time that the character is able to jump for. The greater the value, the further/higher the character will be able to jump.
Next, we have a Boolean, m_IsJumping
, which is true
when the character is jumping and false
otherwise. This will be useful for making sure that the character can't jump while in mid-air.
The m_IsFalling
variable has a similar use to m_IsJumping
. It will be useful to know when a character is falling.
Next, we have two Booleans that will be true if the character's left or right keyboard buttons are currently being pressed. These are relative depending upon the character (A and D for Thomas, Left and Right arrow keys for Bob). How we respond to these Booleans will be seen in the Thomas
and Bob
classes.
The m_TimeThisJump
float variable is updated each and every frame that m_IsJumping
is true
. We can then know when m_JumpDuration
has been reached.
The final protected
variable is the Boolean m_JustJumped
. This will be true
if a jump was initiated in the current frame. It will be useful for knowing when to play a jump sound effect.
Next, add the following private
variables to the PlayableCharacter.h
file:
private: // What is the gravity float m_Gravity; // How fast is the character float m_Speed = 400; // Where is the player Vector2f m_Position; // Where are the characters various body parts? FloatRect m_Feet; FloatRect m_Head; FloatRect m_Right; FloatRect m_Left; // And a texture Texture m_Texture; // All our public functions will come next
In the previous code, we have some interesting private
variables. Remember that these variables will only be directly accessible to the code in the PlayableCharacter
class. The Thomas
and Bob
classes will not be able to access them directly.
The m_Gravity
variable will hold the number of pixels per second that the character will fall. The m_Speed
variable will hold the number of pixels per second that the character can move left or right.
The Vector2f
, m_Position
variable is the position in the world (not the screen) where the center of the character is.
The next four FloatRect
objects are important to discuss. When we did collision detection in the Zombie Arena game, we simply checked to see if two FloatRect
objects intersected. Each FloatRect
object represented an entire character, a pick-up, or a bullet. For the non-rectangular shaped objects (zombies and the player), this was a little bit inaccurate.
In this game, we will need to be more precise. The m_Feet
, m_Head
, m_Right
, and m_Left
FloatRect
objects will hold the coordinates of the different parts of a character's body. These coordinates will be updated in each and every frame.
Through these coordinates, we will be able to tell exactly when a character lands on a platform, bumps his head during a jump, or rubs shoulders with a tile to his side.
Lastly, we have Texture
. Texture
is private
as it is not used directly by the Thomas
or Bob
classes but, as we saw, Sprite
is protected
because it is used directly.
Now add all the public
functions to the PlayableCharacter.h
file and then we will discuss them:
public: void spawn(Vector2f startPosition, float gravity); // This is a pure virtual function bool virtual handleInput() = 0; // This class is now abstract and cannot be instanciated // Where is the player FloatRect getPosition(); // A rectangle representing the position // of different parts of the sprite FloatRect getFeet(); FloatRect getHead(); FloatRect getRight(); FloatRect getLeft(); // Send a copy of the sprite to main Sprite getSprite(); // Make the character stand firm void stopFalling(float position); void stopRight(float position); void stopLeft(float position); void stopJump(); // Where is the center of the character Vector2f getCenter(); // We will call this function once every frame void update(float elapsedTime); };// End of the class
Let's talk about each of the function declarations that we just added. This will make coding their definitions easier to follow.
spawn
function receives a Vector2f
called startPosition
and a float
called gravity
. As the names suggest, startPosition
will be the coordinates in the level at which the character will start and gravity
will be the number of pixels per second at which the character will fall.bool virtual handleInput() = 0
is, of course, our pure virtual function. As PlayableCharacter
has this function, any class that extends it, if we want to instantiate it, must provide a definition for this function. Therefore, when we write all the function definitions for PlayableCharacter
in a minute, we will not provide a definition for handleInput
. There will of course need to be definitions in both the Thomas
and Bob
classes.getPosition
function returns a FloatRect
that represents the position of the whole character.getFeet()
function, as well as getHead
, getRight
, and getLeft
, each return a FloatRect
that represents the location of a specific part of the character's body. This is just what we need for detailed collision detection.getSprite
function, as usual, returns a copy of m_Sprite
to the calling code.stopFalling
, stopRight
, stopLeft
, and stopJump
function receive a single float
value, which the function will use to reposition the character and stop it walking or jumping through a solid tile.getCenter
function returns a Vector2f
to the calling code to let it know exactly where the center of the character is. This value is, of course, held in m_Position
. We will see later that it is used by the Engine
class to center the appropriate View
around the appropriate character.update
function we have seen many times before and as usual, it takes a float
parameter, which is the fraction of a second that the current frame has taken. This update
function will need to do more work than previous update
functions (from other projects), however. It will need to handle jumping, as well as updating the FloatRect
objects that represent the head, feet, left, and right.Now we can write the definitions for all the functions, except, of course, handleInput
.
Right-click 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 PlayableCharacter.cpp
. Finally, click the Add button. We are now ready to code the .cpp
file for the PlayableCharacter
class.
We will break up the code and discussion into a number of chunks. First, add the include directives and the definition of the spawn
function:
#include "stdafx.h" #include "PlayableCharacter.h" void PlayableCharacter::spawn(Vector2f startPosition, float gravity) { // Place the player at the starting point m_Position.x = startPosition.x; m_Position.y = startPosition.y; // Initialize the gravity m_Gravity = gravity; // Move the sprite in to position m_Sprite.setPosition(m_Position); }
The spawn
function initializes m_Position
with the passed-in position, as well as initializing m_Gravity
. The final line of code moves m_Sprite
to its starting position.
Next, add the definition for the update
function, immediately after the preceding code:
void PlayableCharacter::update(float elapsedTime) { if (m_RightPressed) { m_Position.x += m_Speed * elapsedTime; } if (m_LeftPressed) { m_Position.x -= m_Speed * elapsedTime; } // Handle Jumping if (m_IsJumping) { // Update how long the jump has been going m_TimeThisJump += elapsedTime; // Is the jump going upwards if (m_TimeThisJump < m_JumpDuration) { // Move up at twice gravity m_Position.y -= m_Gravity * 2 * elapsedTime; } else { m_IsJumping = false; m_IsFalling = true; } } // Apply gravity if (m_IsFalling) { m_Position.y += m_Gravity * elapsedTime; } // Update the rect for all body parts FloatRect r = getPosition(); // Feet m_Feet.left = r.left + 3; m_Feet.top = r.top + r.height - 1; m_Feet.width = r.width - 6; m_Feet.height = 1; // Head m_Head.left = r.left; m_Head.top = r.top + (r.height * .3); m_Head.width = r.width; m_Head.height = 1; // Right m_Right.left = r.left + r.width - 2; m_Right.top = r.top + r.height * .35; m_Right.width = 1; m_Right.height = r.height * .3; // Left m_Left.left = r.left; m_Left.top = r.top + r.height * .5; m_Left.width = 1; m_Left.height = r.height * .3; // Move the sprite into position m_Sprite.setPosition(m_Position); }
The first two parts of the code check whether m_RightPressed
or m_LeftPressed
is true
. If either of them is, m_Position
is changed using the same formula as the previous project (elapsed time multiplied by speed).
Next, we see whether or not the character is currently executing a jump. We know this from if(m_IsJumping)
. If this if
statement is true
, these are the steps the code takes:
m_TimeThisJump
with elapsedTime
.m_TimeThisJump
is still less than m_JumpDuration
. If it is, change the y coordinate of m_Position
by twice gravity multiplied by the elapsed time.else
clause that executes when m_TimeThisJump
is not lower than m_JumpDuration
, then m_Falling
is set to true
. The effect of doing this will be seen next. Also, m_Jumping
is set to false
. This prevents the code we have just been discussing from executing, because if(m_IsJumping)
is now false.The if(m_IsFalling)
block moves m_Position
down each frame. It is moved using the current value of m_Gravity
and the elapsed time.
The following code (almost all of the remaining code) updates the body parts of the character, relative to the current position of the sprite as a whole. Take a look at the following diagram to see how the code calculates the position of the virtual head, feet, left, and right sides of the character:
The final line of code uses the setPosition
function to move the sprite to its correct location after all of the possibilities of the update
function.
Now add the definition for the getPosition
, getCenter
, getFeet
, getHead
, getLeft
, getRight
, and getSprite
functions, immediately after the previous code:
FloatRect PlayableCharacter::getPosition() { return m_Sprite.getGlobalBounds(); } Vector2f PlayableCharacter::getCenter() { return Vector2f( m_Position.x + m_Sprite.getGlobalBounds().width / 2, m_Position.y + m_Sprite.getGlobalBounds().height / 2 ); } FloatRect PlayableCharacter::getFeet() { return m_Feet; } FloatRect PlayableCharacter::getHead() { return m_Head; } FloatRect PlayableCharacter::getLeft() { return m_Left; } FloatRect PlayableCharacter::getRight() { return m_Right; } Sprite PlayableCharacter::getSprite() { return m_Sprite; }
The getPosition
function returns a FloatRect
that wraps the entire sprite, and getCenter
returns a Vector2f
, which contains the center of the sprite. Notice that we divide the height and width of the sprite by two in order to dynamically arrive at this result. This is because Thomas and Bob will be of different heights.
The getFeet
, getHead
, getLeft
, and getRight
functions return the FloatRect
objects that represent the body parts of the character that we update each frame in the update
function. We will write the collision detection code that uses these functions in the following chapter.
The getSprite
function, as usual, returns a copy of m_Sprite
.
Finally, for the PlayableCharacter
class, add the definitions for the stopFalling
, stopRight
, stopLeft
, and stopJump
functions. Do so immediately after the previous code:
void PlayableCharacter::stopFalling(float position) { m_Position.y = position - getPosition().height; m_Sprite.setPosition(m_Position); m_IsFalling = false; } void PlayableCharacter::stopRight(float position) { m_Position.x = position - m_Sprite.getGlobalBounds().width; m_Sprite.setPosition(m_Position); } void PlayableCharacter::stopLeft(float position) { m_Position.x = position + m_Sprite.getGlobalBounds().width; m_Sprite.setPosition(m_Position); } void PlayableCharacter::stopJump() { // Stop a jump early m_IsJumping = false; m_IsFalling = true; }
Each of the previous functions receives a value as a parameter that is used to reposition either the top, bottom, left, or right of the sprite. Exactly what these values are and how they are obtained will be seen in the following chapter. Each of the previous functions also repositions the sprite.
The final function is the stopJump
function that will also be used in collision detection. It sets the necessary values for m_IsJumping
and m_IsFalling
to end a jump.
52.15.55.18