Chapter 15: Advanced OOP – Inheritance and Polymorphism

In this chapter, we will further extend our knowledge of OOP by looking at the slightly more advanced concepts of inheritance and polymorphism. We will then be able to use this new knowledge to implement the star characters of our game, Thomas and Bob. Here is what we will cover in this chapter:

  • Learn how to extend and modify a class using inheritance
  • Treat an object of a class as if it is more than one type of class by using polymorphism
  • Learn about abstract classes and how designing classes that are never instantiated can actually be useful
  • Build an abstract PlayableCharacter class
  • Put inheritance to work with the Thomas and Bob classes
  • Add Thomas and Bob to the game project

Inheritance

We have already seen how we can use other people's hard work by instantiating objects from the classes of the SFML library. But this whole OOP thing goes even further than that.

What if there is a class that has loads of useful functionality in it, but is not quite what we want? In this situation, we can inherit from the other class. Just like it sounds, inheritance means we can harness all the features and benefits of other people's classes, including the encapsulation, while further refining or extending the code specifically to our situation. In this project, we will inherit from and extend some SFML classes; we will also do so with our own classes.

Let's look at some code that uses inheritance.

Extending a class

With all this in mind, let's look at an example class and see how we can extend it, just to see the syntax and as a first step.

First, we define a class to inherit from. This is no different from how we created any of our other classes. Take a look at this hypothetical Soldier class declaration:

class Soldier

{

    private:

        // How much damage can the soldier take

        int m_Health;

        int m_Armour;

        int m_Range;

        int m_ShotPower;

        

    

    Public:

        void setHealth(int h);

        void setArmour(int a);    

        void setRange(int r);

        void setShotPower(int p);

};

In the previous code, we define a Soldier class. It has four private variables: m_Health, m_Armour, m_Range, and m_ShotPower. It has also four public functions: setHealth, setArmour, setRange, and setShotPower. We don't need to see the definitions of these functions; they will simply initialize the appropriate variable that their name makes obvious.

We can also imagine that a fully implemented Soldier class would be much more in-depth than this. It would probably have functions such as shoot, goProne, and so on. If we implemented a Soldier class in an SFML project, it would likely have a Sprite object, as well as an update and a getPostion function.

The simple scenario that we've presented here is suitable if we wish to learn about inheritance. Now, let's look at something new: inheriting from the Soldier class. Look at the following code, especially the highlighted part:

class Sniper : public Soldier

{

public:

    // A constructor specific to Sniper

    Sniper::Sniper();

};

By adding : public Soldier to the Sniper class declaration, Sniper inherits from Soldier. But what does this mean, exactly? Sniper is a Soldier. It has all the variables and functions of Soldier. Inheritance is even more than this, however.

Also note that, in the previous code, we declare a Sniper constructor. This constructor is unique to Sniper. We have not only inherited from Soldier; we have extended Soldier. All the functionality (definitions) of the Soldier class would be handled by the Soldier class, but the definition of the Sniper constructor must be handled by the Sniper class.

Here is what the hypothetical Sniper constructor definition might look like:

// In Sniper.cpp

Sniper::Sniper()

{

    setHealth(10);

    setArmour(10);    

    setRange(1000);

    setShotPower(100);

}

We could go ahead and write a bunch of other classes that are extensions of the Soldier class, perhaps Commando and Infantryman. Each would have the exact same variables and functions, but each could also have a unique constructor that initializes those variables appropriate to the specific type of Soldier. Commando might have very high m_Health and m_ShotPower but really puny m_Range. Infantryman might be in between Commando and Sniper with mediocre values for each variable.

As if OOP wasn't useful enough already, we can now model real-world objects, including their hierarchies. We can achieve this by sub-classing/extending/inheriting from other classes.

The terminology we might like to learn here is that the class that is extended from is the super-class, and the class that inherits from the super-class is the sub-class. We can also say parent and child class.

Tip

You might find yourself asking this question about inheritance: why? The reason is something like this: we can write common code once; in the parent class, we can update that common code and all the classes that inherit from it are also updated. Furthermore, a sub-class only gets to use public and protected instance variables and functions. So, designed properly, this also enhances the goals of encapsulation.

Did you say protected? Yes. There is an access specifier for class variables and functions called protected. You can think of protected variables as being somewhere between public and private. Here is a quick summary of access specifiers, along with more details about the protected specifier:

  • Public variables and functions can be accessed and used by anyone with an instance of the class.
  • Private variables and functions can only accessed/used by the internal code of the class, and not directly from an instance. This is good for encapsulation and when we need to access/change private variables, since we can provide public getter and setter functions (such as getSprite). If we extend a class that has private variables and functions, that child class cannot directly access the private data of its parent.
  • Protected variables and functions are almost the same as private. They cannot be accessed/used directly by an instance of the class. However, they can be used directly by any class that extends the class they are declared in. So, it is like they are private, except to child classes.

To fully understand what protected variables and functions are and how they can be useful, let's look at another topic first. Then, we will see them in action.

Polymorphism

Polymorphism allows us to write code that is less dependent on the types we are trying to manipulate. This can make our code clearer and more efficient. Polymorphism means many forms. If the objects that we code can be more than one type of thing, then we can take advantage of this.

Important note

But what does polymorphism mean to us? Boiled down to its simplest definition, polymorphism means the following: any sub-class can be used as part of the code that uses the super-class. This means we can write code that is simpler and easier to understand and also easier to modify or change. Also, we can write code for the super-class and rely on the fact that no matter how many times it is sub-classed, within certain parameters, the code will still work.

Let's discuss an example.

Suppose we want to use polymorphism to help write a zoo management game where we must feed and tend to the needs of animals. We will probably want to have a function such as feed. We will also probably want to pass an instance of the animal to be fed into the feed function.

A zoo, of course, has lots of animals, such as Lion, Elephant, and Three-toed Sloth. With our new knowledge of C++ inheritance, it makes sense to code an Animal class and have all the different types of animal inherit from it.

If we want to write a function (feed) that we can pass Lion, Elephant, and ThreeToedSloth into as a parameter, it might seem like we need to write a feed function for each type of Animal. However, we can write polymorphic functions with polymorphic return types and arguments. Take a look at the following definition of the hypothetical feed function:

void feed(Animal& a)

{

    a.decreaseHunger();

}

The preceding function has an Animal reference as a parameter, meaning that any object that is built from a class that extends Animal can be passed into it.

This means you can write code today and make another subclass in a week, month, or year, and the very same functions and data structures will still work. Also, we can enforce a set of rules upon our subclasses regarding what they can and cannot do, as well as how they do it. So, good design in one stage can influence it at other stages.

But will we ever really want to instantiate an actual Animal?

Abstract classes – virtual and pure virtual functions

An abstract class is a class that cannot be instantiated and therefore cannot be made into an object.

Tip

Some terminology we might like to learn about here is concrete class. A concrete class is any class that isn't abstract. In other words, all the classes we have written so far have been concrete classes and can be instantiated into usable objects.

So, it's code that will never be used, then? But that's like paying an architect to design your home and then never building it!

If we, or the designer of a class, wants to force its users to inherit it before using their class, they can make a class abstract. If this happens, we cannot make an object from it; therefore, we must inherit from it first and make an object from the sub-class.

To do so, we can make a function pure virtual and not provide any definition. Then, that function must be overridden (rewritten) in any class that inherits from it.

Let's look at an example; it will help. We can make a class abstract by adding a pure virtual function such as the abstract Animal class, which can only perform the generic action of makeNoise:

Class Animal

    private:

        // Private stuff here

    public:

        void virtual makeNoise() = 0;

        // More public stuff here

};

As you can see, we add the C++ keyword virtual, before, and = 0 after the function declaration. Now, any class that extends/inherits from Animal must override the makeNoise function. This might make sense since different types of animal make very different types of noise. We could have assumed that anybody who extends the Animal class is smart enough to notice that the Animal class cannot make a noise and that they will need to handle it, but what if they don't notice? The point is that by making a pure virtual function, we guarantee that they will, because they must.

Abstract classes are also useful because, sometimes, we want a class that can be used as a polymorphic type, but we need to guarantee it can never be used as an object. For example, Animal doesn't really make sense on its own. We don't talk about animals; we talk about types of animals. We don't say, "Ooh, look at that lovely, fluffy, white animal!", or, "Yesterday we went to the pet shop and got an animal and an animal bed." It's just too, well, abstract.

So, an abstract class is kind of like a template to be used by any class that extends it (inherits from it). If we were building an Industrial Empire type game where the player manages businesses and their employees, we might want a Worker class, for example, and extend it to make Miner, Steelworker, OfficeWorker, and, of course, Programmer. But what exactly does a plain Worker do? Why would we ever want to instantiate one?

The answer is we wouldn't want to instantiate one, but we might want to use it as a polymorphic type so that we can pass multiple Worker sub-classes between functions and have data structures that can hold all types of workers.

All pure virtual functions must be overridden by any class that extends the parent class that contains the pure virtual function. This means that the abstract class can provide some of the common functionality that would be available in all its subclasses. For example, the Worker class might have the m_AnnualSalary, m_Productivity, and m_Age member variables. It might also have the getPayCheck function, which is not pure virtual and is the same in all the sub-classes, but a doWork function, which is pure virtual and must be overridden, because all the different types of Worker will doWork very differently.

Important note

By the way, virtual as opposed to pure virtual is a function that can be optionally overridden. You declare a virtual function the same way as a pure virtual function but leave the = 0 off to the end. In the current game project, we will use a pure virtual function.

If any of this virtual, pure virtual, or abstract stuff is unclear, using it is probably the best way to understand it.

Building the PlayableCharacter class

Now that we know the basics of inheritance, polymorphism, and pure virtual functions, we will put them to use. We will build a PlayableCharacter class that has most of the functionality that any character from our game is going to need. It will have one pure virtual function, known as 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 the 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. It will not be possible to instantiate a PlayableCharacter instance directly, but we wouldn't want to because it is too abstract anyway.

Coding PlayableCharacter.h

As usual when creating a class, we will start off with the header file that will contain the member variable 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 were public in classes that 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 will be the protected section, followed by private, and then public.

Add the following code 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 initiated 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 inherit from the class, all the variables we just wrote will be accessible to those classes that extend it. We will extend this class with the Thomas and Bob classes.

Tip

The terms inherit from and extend are virtually synonymous in most contexts in this book. Sometimes, one seems more appropriate than the other, however.

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. If we do, 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 variable 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 used 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 relatively dependent upon the character (A and D for Thomas, and the Left and Right arrow keys for Bob, respectively). How we respond to these Booleans will be seen in the Thomas and Bob classes.

The  m_TimeThisJump float variable is updated each frame that m_IsJumping is true. We can then find out when m_JumpDuration has been reached.

The final protected variable is the m_JustJumped Boolean. This will be true if a jump was initiated in the current frame. It will be used so that we know 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 pickup, 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,  m_Left, and FloatRect objects will hold the coordinates of the different parts of a character's body. These coordinates will be updated each frame.

Through these coordinates, we will be able to tell exactly when a character lands on a platform, bumps their head during a jump, or rubs shoulders with a tile to their side.

Lastly, we have a Texture. The Texture is private as it is not used directly by the Thomas or Bob classes. However, as we saw, the Sprite is protected because it is used directly.

Now, add all the public functions to the PlayableCharacter.h file. 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 instantiated

    // 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:

  • The spawn function receives a Vector2f called startPosition and a float value called gravity. As the names suggest, startPosition will be the coordinates in the level that 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. Since 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 need to be definitions in both the Thomas and Bob classes.
  • The getPosition function returns a FloatRect object that represents the position of the whole character.
  • The getFeet() function, as well as getHead, getRight, and getLeft, return a FloatRect object that represents the location of a specific part of the character's body. This is just what we need for detailed collision detection.
  • The getSprite function, as usual, returns a copy of m_Sprite to the calling code.
  • The stopFalling, stopRight, stopLeft, and stopJump functions receive a single float value that the function will use to reposition the character and stop it walking or jumping through a solid tile.
  • The getCenter function returns a Vector2f object to the calling code to let it know exactly where the center of the character is. This value is held in m_Position. As we will see later, it is used by the Engine class to center the appropriate View around the appropriate character.
  • We have seen the update function many times before and, as usual, it takes a float parameter, which is the fraction of a second the current frame has taken. This update function will need to do more work than previous update functions (from our other projects), however. It will need to handle jumping as well as updating the FloatRect objects that represent the head, feet, and left- and right-hand sides of the character.

Now, we can write the definitions for all the functions, except, of course, handleInput.

Coding PlayableCharacter.cpp

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 discuss it in several chunks. First, add the include directives and the definition of the spawn function:

#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, and also initializes 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 previous 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, then m_Position is changed using the same formula as the previous project (elapsed time multiplied by speed).

Next, we see whether 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:

  1. Updates m_TimeThisJump with elapsedTime.
  2. Checks if m_TimeThisJump is still less than m_JumpDuration. If it is, it changes the y coordinate of m_Position by 2x gravity, multiplied by the elapsed time.
  3. In the else clause that executes when m_TimeThisJump is not lower than m_JumpDuration, 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 code that follows (most 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, and left- and right-hand sides of the character:

The final line of code uses the setPosition function to move the sprite to its correct location after all the possibilities of the update function.

Now, add the definitions 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, while getCenter returns a Vector2f that contains the center of the sprite. Notice that we divide the height and width of the sprite by 2 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 next 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 something for the next chapter. Each of the previous functions also repositions the sprite.

The final function is the stopJump function, which will also be used in collision detection. It sets the necessary values for m_IsJumping and m_IsFalling to end a jump.

Building the Thomas and Bob classes

Now, we get to use inheritance for real. We will build a class for Thomas and a class for Bob. They will both inherit from the PlayableCharacter class we just coded. They will then have all the functionality of the PlayableCharacter class, including direct access to its protected variables. We will also add the definition for the pure virtual function, handleInput. You will notice that the handleInput functions for Thomas and Bob will be different.

Coding Thomas.h

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 Thomas.h. Finally, click the Add button. We are now ready to code the header file for the Thomas class.

Add the following code to the Thomas.h class:

#pragma once

#include "PlayableCharacter.h"

class Thomas : public PlayableCharacter

{

public:

    // A constructor specific to Thomas

    Thomas::Thomas();

    // The overridden input handler for Thomas

    bool virtual handleInput();

};

The previous code is very short and sweet. We can see that we have a constructor and that we are going to implement the pure virtual handleInput function. So, let's do that now.

Coding Thomas.cpp

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 Thomas.cpp. Finally, click the Add button. We are now ready to code the .cpp file for the Thomas class.

Add the Thomas constructor to the Thomas.cpp file, as follows:

#include "Thomas.h"

#include "TextureHolder.h"

Thomas::Thomas()

{

    // Associate a texture with the sprite

    m_Sprite = Sprite(TextureHolder::GetTexture(

        "graphics/thomas.png"));

    m_JumpDuration = .45;

}

All we need to do is load the thomas.png graphic and set the duration of a jump (m_JumpDuration) to .45 (nearly half a second).

Add the definition of the handleInput function as follows:

// A virtual function

bool Thomas::handleInput()

{

    m_JustJumped = false;

    if (Keyboard::isKeyPressed(Keyboard::W))

    {

        // Start a jump if not already jumping

        // but only if standing on a block (not falling)

        if (!m_IsJumping && !m_IsFalling)

        {

            m_IsJumping = true;

            m_TimeThisJump = 0;

            m_JustJumped = true;

        }

    }

    else

    {

        m_IsJumping = false;

        m_IsFalling = true;

    }

    if (Keyboard::isKeyPressed(Keyboard::A))

    {

        m_LeftPressed = true;

    }

    else

    {

        m_LeftPressed = false;

    }

    if (Keyboard::isKeyPressed(Keyboard::D))

    {

        m_RightPressed = true;

    }

    else

    {

        m_RightPressed = false;

    }

    return m_JustJumped;

}

This code should look quite familiar to you. We are using the SFML isKeyPressed function to see whether any of the W, A, or D keys are being pressed.

When W is pressed, the player is attempting to jump. The code then uses the if(!m_IsJumping && !m_IsFalling) code to check that the character is not already jumping and that it is not falling either. When these tests are both true, m_IsJumping is set to true, m_TimeThisJump is set to 0, and m_JustJumped is set to true.

When the previous two tests don't evaluate to true, the else clause is executed and m_Jumping is set to false, and m_IsFalling is set to true.

Handling how the A and D keys are being pressed is as simple as setting m_LeftPressed and/or m_RightPressed to true or false. The update function will now be able to handle moving the character.

The last line of code in the function returns the value of m_JustJumped. This will let the calling code know if it needs to play a jumping sound effect.

We will now code the Bob class. It is nearly identical to the Thomas class, except it has different jumping abilities and a different Texture, and uses different keys on the keyboard.

Coding Bob.h

The Bob class is identical in structure to the Thomas class. It inherits from PlayableCharacter, it has a constructor, and it provides the definition of the handleInput function. The difference compared to Thomas is that we initialize some of Bob's member variables differently and we handle input (in the handleInput function) differently as well. Let's code the class and look at the details.

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 Bob.h. Finally, click the Add button. We are now ready to code the header file for the Bob class.

Add the following code to the Bob.h file:

#pragma once

#include "PlayableCharacter.h"

class Bob : public PlayableCharacter

{

public:

    // A constructor specific to Bob

    Bob::Bob();

    // The overriden input handler for Bob

    bool virtual handleInput();

};

The previous code is identical to the Thomas.h file apart from the class name and therefore the constructor name.

Coding Bob.cpp

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 Bob.cpp. Finally, click the Add button. We are now ready to code the .cpp file for the Bob class.

Add the following code for the Bob constructor to the Bob.cpp file. Notice that the texture is different (bob.png) and that m_JumpDuration is initialized to a significantly smaller value. Bob is now his own unique self:

#include "Bob.h"

#include "TextureHolder.h"

Bob::Bob()

{

    // Associate a texture with the sprite

    m_Sprite = Sprite(TextureHolder::GetTexture(

        "graphics/bob.png"));

    m_JumpDuration = .25;

}

Add the handleInput code immediately after the Bob constructor:

bool Bob::handleInput()

{

    m_JustJumped = false;

    if (Keyboard::isKeyPressed(Keyboard::Up))

    {

        // Start a jump if not already jumping

        // but only if standing on a block (not falling)

        if (!m_IsJumping && !m_IsFalling)

        {

            m_IsJumping = true;

            m_TimeThisJump = 0;

            m_JustJumped = true;

        }

    }

    else

    {

        m_IsJumping = false;

        m_IsFalling = true;

    }

    if (Keyboard::isKeyPressed(Keyboard::Left))

    {

        m_LeftPressed = true;

    }

    else

    {

        m_LeftPressed = false;

    }

    if (Keyboard::isKeyPressed(Keyboard::Right))

    {

        m_RightPressed = true;;

    }

    else

    {

        m_RightPressed = false;

    }

    return m_JustJumped;

}

Notice that the code is nearly identical to the code in the handleInput function of the Thomas class. The only difference is that we respond to different keys (the Left arrow key and Right arrow key for left and right movement, respectively, and the Up arrow key for jumping).

Now that we have a PlayableCharacter class that has been extended by the Bob and Thomas classes, we can add a Bob and a Thomas instance to the game.

Updating the game engine to use Thomas and Bob

In order to be able to run the game and see our new characters, we have to declare instances of them, call their spawn functions, update them each frame, and draw them each frame. Let's do that now.

Updating Engine.h to add an instance of Bob and Thomas

Open up the Engine.h file and add the following highlighted lines of code:

#pragma once

#include <SFML/Graphics.hpp>

#include "TextureHolder.h"

#include "Thomas.h"

#include "Bob.h"

using namespace sf;

class Engine

{

private:

    // The texture holder

    TextureHolder th;

    // Thomas and his friend, Bob

    Thomas m_Thomas;

    Bob m_Bob;

    const int TILE_SIZE = 50;

    const int VERTS_IN_QUAD = 4;

    ...

    ...

Now, we have an instance of both Thomas and Bob that are derived from PlayableCharacter.

Updating the input function to control Thomas and Bob

Now, we will add the ability to control the two characters. This code will go in the input part of the code. Of course, for this project, we have a dedicated input function. Open Input.cpp and add the following highlighted code:

void Engine::input()

{

    Event event;

    while (m_Window.pollEvent(event))

    {

        if (event.type == Event::KeyPressed)

        {

            // Handle the player quitting

            if (Keyboard::isKeyPressed(Keyboard::Escape))

            {

                m_Window.close();

            }

            // Handle the player starting the game

            if (Keyboard::isKeyPressed(Keyboard::Return))

            {

                m_Playing = true;

            }

            // Switch between Thomas and Bob

            if (Keyboard::isKeyPressed(Keyboard::Q))

            {

                m_Character1 = !m_Character1;

            }

            // Switch between full and split-screen

            if (Keyboard::isKeyPressed(Keyboard::E))

            {

                m_SplitScreen = !m_SplitScreen;

            }

        }

    }

    // Handle input specific to Thomas

    if(m_Thomas.handleInput())

    {

        // Play a jump sound

    }

    // Handle input specific to Bob

    if(m_Bob.handleInput())

    {

        // Play a jump sound

    }

}

Note how simple the previous code is: all the functionality is contained within the Thomas and Bob classes. All the code must do is add an include directive for each of the Thomas and Bob classes. Then, within the input function, the code just calls the pure virtual handleInput functions on m_Thomas and m_Bob. The reason we wrap each of the calls in an if statement is that they return true or false based on whether a new jump has just been successfully initiated. We will handle playing the jump sound effects in Chapter 17, Sound Spatialization and the HUD.

Updating the update function to spawn and update the PlayableCharacter instances

This is broken into two parts. First, we need to spawn Bob and Thomas at the start of a new level, and second, we need to update them (by calling their update functions) each frame.

Spawning Thomas and Bob

We need to call the spawn functions of our Thomas and Bob objects in a few different places as the project progresses. Most obviously, we need to spawn the two characters when a new level begins. In the next chapter, as the number of tasks we need to perform at the beginning of a level increases, we will write a loadLevel function. For now, let's just call spawn on m_Thomas and m_Bob in the update function, as highlighted in the following code. Add the following code, but keep in mind that it will eventually be deleted and replaced:

void Engine::update(float dtAsSeconds)

{

    if (m_NewLevelRequired)

    {

        // These calls to spawn will be moved to a new

        // loadLevel() function soon

        // Spawn Thomas and Bob

        m_Thomas.spawn(Vector2f(0,0), GRAVITY);

        m_Bob.spawn(Vector2f(100, 0), GRAVITY);

        // Make sure spawn is called only once

        m_TimeRemaining = 10;

        m_NewLevelRequired = false;

    }

    if (m_Playing)

    {

        // Count down the time the player has left

        m_TimeRemaining -= dtAsSeconds;

        // Have Thomas and Bob run out of time?

        if (m_TimeRemaining <= 0)

        {

            m_NewLevelRequired = true;

        }

    }// End if playing

        

}

The previous code simply calls spawn and passes in a location in the game world, along with the gravity. The code is wrapped in an if statement that checks whether a new level is required. The spawning code will be moved to a dedicated loadLevel function, but the if condition will be part of the finished project. Also, m_TimeRemaining is set to an arbitrary 10 seconds for now.

Now, we can update the instances each frame of the game loop.

Updating Thomas and Bob each frame

Next, we will update Thomas and Bob. All we need to do is call their update functions and pass in the time this frame has taken.

Add the following highlighted code:

void Engine::update(float dtAsSeconds)

{

    if (m_NewLevelRequired)

    {

        // These calls to spawn will be moved to a new

        // LoadLevel function soon

        // Spawn Thomas and Bob

        m_Thomas.spawn(Vector2f(0,0), GRAVITY);

        m_Bob.spawn(Vector2f(100, 0), GRAVITY);

        // Make sure spawn is called only once

        m_NewLevelRequired = false;

    }

    if (m_Playing)

    {

        // Update Thomas

        m_Thomas.update(dtAsSeconds);

        // Update Bob

        m_Bob.update(dtAsSeconds);

        // Count down the time the player has left

        m_TimeRemaining -= dtAsSeconds;

        // Have Thomas and Bob run out of time?

        if (m_TimeRemaining <= 0)

        {

            m_NewLevelRequired = true;

        }

    }// End if playing

        

}

Now that the characters can move, we need to update the appropriate View objects to center around the characters and make them the center of attention. Of course, until we have some objects in our game world, the sensation of actual movement will not be achieved.

Add the following highlighted code:

void Engine::update(float dtAsSeconds)

{

    if (m_NewLevelRequired)

    {

        // These calls to spawn will be moved to a new

        // LoadLevel function soon

        // Spawn Thomas and Bob

        m_Thomas.spawn(Vector2f(0,0), GRAVITY);

        m_Bob.spawn(Vector2f(100, 0), GRAVITY);

        // Make sure spawn is called only once

        m_NewLevelRequired = false;

    }

    if (m_Playing)

    {

        // Update Thomas

        m_Thomas.update(dtAsSeconds);

        // Update Bob

        m_Bob.update(dtAsSeconds);

        // Count down the time the player has left

        m_TimeRemaining -= dtAsSeconds;

        // Have Thomas and Bob run out of time?

        if (m_TimeRemaining <= 0)

        {

            m_NewLevelRequired = true;

        }

    }// End if playing

        

    // Set the appropriate view around the appropriate character

    if (m_SplitScreen)

    {

        m_LeftView.setCenter(m_Thomas.getCenter());

        m_RightView.setCenter(m_Bob.getCenter());

    }

    else

    {

        // Centre full screen around appropriate character

        if (m_Character1)

        {

            m_MainView.setCenter(m_Thomas.getCenter());

        }

        else

        {

            m_MainView.setCenter(m_Bob.getCenter());

        }

    }

}

The previous code handles the two possible situations. First, the if(mSplitScreen) condition positions the left-hand view around m_Thomas and the right-hand view around m_Bob. The else clause that executes when the game is in full screen mode tests to see if m_Character1 is true. If it is, then the full screen view (m_MainView) is centered around Thomas, otherwise it is centered around Bob. You probably remember that the player can use the E key to toggle split-screen mode and the Q key to toggle between Bob and Thomas in full screen mode. We coded this in the input function of the Engine class, back in Chapter 12, Layering Views and Implementing the HUD.

Now, we can draw the graphics for Thomas and Bob to the screen.

Drawing Bob and Thomas

Make sure the Draw.cpp file is open and add the following highlighted code:

void Engine::draw()

{

    // Rub out the last frame

    m_Window.clear(Color::White);

    if (!m_SplitScreen)

    {

        // Switch to background view

        m_Window.setView(m_BGMainView);

        // Draw the background

        m_Window.draw(m_BackgroundSprite);

        // Switch to m_MainView

        m_Window.setView(m_MainView);        

        // Draw thomas

        m_Window.draw(m_Thomas.getSprite());

        // Draw bob

        m_Window.draw(m_Bob.getSprite());

    }

    else

    {

        // Split-screen view is active

        // First draw Thomas' side of the screen

        // Switch to background view

        m_Window.setView(m_BGLeftView);

        // Draw the background

        m_Window.draw(m_BackgroundSprite);

        // Switch to m_LeftView

        m_Window.setView(m_LeftView);

        // Draw bob

        m_Window.draw(m_Bob.getSprite());

    

        // Draw thomas

        m_Window.draw(m_Thomas.getSprite());

        

        // Now draw Bob's side of the screen

        // Switch to background view

        m_Window.setView(m_BGRightView);

        // Draw the background

        m_Window.draw(m_BackgroundSprite);

        // Switch to m_RightView

        m_Window.setView(m_RightView);

        // Draw thomas

        m_Window.draw(m_Thomas.getSprite());

        // Draw bob

        m_Window.draw(m_Bob.getSprite());

                

    }

    // Draw the HUD

    // Switch to m_HudView

    m_Window.setView(m_HudView);

    

    

    // Show everything we have just drawn

    m_Window.display();

}

Notice that we draw both Thomas and Bob for full screen, the left, and the right. Also, notice the very subtle difference in the way that we draw the characters in split-screen mode. When drawing the left-hand side of the screen, we switch the order the characters are drawn and draw Thomas after Bob. So, Thomas will always be "on top" on the left and Bob will always be on top on the right. This is because the player controlling Thomas is catered for on the left and Bob the right, respectively.

You can now run the game and see Thomas and Bob in the center of the screen, as follows:

If you press the Q key to switch focus from Thomas to Bob, you will see the View make the slight adjustment. If you move either of the characters left or right (Thomas with A and D, and Bob with the arrow keys) you will see them move relative to each other.

Try pressing the E key to toggle between full screen and split-screen. Then, try moving both characters again to see the effect. In the following screenshot, you can see that Thomas is always centered in the left-hand window and Bob is always centered in the right-hand window:

If you leave the game running long enough, the characters will respawn in their original positions every 10 seconds. This is the beginning of the functionality we will need for the finished game. This behavior is caused by m_TimeRemaining going below 0 and then setting the m_NewLevelRequired variable to true.

Also note that we can't see the full effect of movement until we draw the details of the level. In fact, although it can't be seen, both characters are continuously falling at 300 pixels per second. Since the camera is centering around them every frame and there are no other objects in the game world, we cannot see this downward movement.

If you want to see this for yourself, just change the call to m_Bob.spawn, as follows:

m_Bob.spawn(Vector2f(0,0), 0);

Now that Bob has no gravitational effect, Thomas will visibly fall away from him. This is shown in the following screenshot:

We will add some playable levels to interact with in the next chapter.

Summary

In this chapter, we learned about some new C++ concepts, such as inheritance, which allows us to extend a class and gain all its functionality. We also learned that we can declare variables as protected and that this will give the child class access to them, but they will still be encapsulated (hidden) from all other code. We also used pure virtual functions, which make a class abstract, meaning that the class cannot be instantiated and must therefore be inherited from/extended. We were also introduced to the concept of polymorphism, but will need to wait until the next chapter to use it in our game.

In the next chapter, we will add some major functionality to the game. By the end of the next chapter, Thomas and Bob will be walking, jumping, and falling. They will even be able to jump on each other's heads, as well as exploring some level designs that have been loaded from a text file.

FAQ

Q) We learned about Polymorphism, but why didn't I notice anything polymorphic in the game code so far?

A) We will see polymorphism in action in the next chapter when we write a function that takes PlayerCharacter as a parameter. We will see how we can pass both Bob and Thomas to this new function. It will work the same with both of them.

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

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