Procedural Content Generation for C++ Game Development
上QQ阅读APP看书,第一时间看更新

Polymorphism

Before we get started with the game template, we're going to take a look at polymorphism. It's an important feature of object-orientated programming that we will be taking advantage of in many of the procedural systems that we will create. Therefore, it's important that you have a solid understanding of not only what it is, but also the techniques that are used to achieve it and the potential pitfalls.

Tip

If you already have a strong understanding of polymorphism, feel free to skip this section or head to https://msdn.microsoft.com/en-us/library/z165t2xk(v=vs.90) for a more in-depth discussion of the topic.

Polymorphism is the ability to access different objects through an individually implemented common interface. That's a very formal definition. So, let's break that down into the individual techniques and features that are used to achieve it. It's worth noting that while polymorphism is the standard approach in the games industry, it's still a choice among other schools of programming.

Inheritance

Inheritance is perhaps the key component in achieving polymorphism. Inheritance is extending an existing class by inheriting its variables and functions, and then adding your own.

Let's take a look at a typical game example. Let's assume that we have a game with three different weapons: a sword, a wand, and an axe. These classes will share some common variables such as attack strength, durability, and attack speed. It would be a waste to create three individual classes and add this information to each, so instead we will create a parent class that includes all the shared information. Then, the children will inherit these values and use them the way they want.

Inheritance creates an "is a" relationship. This means that since Axe is inherited from Weapon, Axe is a Weapon. This concept of creating a common interface in a parent class and implementing it in unique ways via child classes is the key to achieving polymorphism.

Note

By interface, I mean the collection of functions and variables that the parent class passes to its children.

The following diagram illustrates this scenario in the form of a simple class diagram:

The highlighted Attack() functions in the individual weapons are all inherited from the single Attack() function defined in the Weapon class.

Tip

To maintain proper encapsulation and scope, it's important that our variables and functions are given the correct visibility modifiers. If you're unsure about this or you could do with a quick reminder, head to https://msdn.microsoft.com/en-us/library/kktasw36.aspx.

Virtual functions

Continuing with the generic weapon example, we now have a parent class that provides a number of functions and variables that all child classes will inherit. In order to be able to denote our own behavior that is different from that of the parent class, we need to be able to override the parent functions. This is achieved through the use of virtual functions.

Virtual functions are functions that can be overridden by implementing classes. In order for this to be possible, the parent class must mark the function as virtual. This is done by simply prefixing the virtual keyword to a function declaration like this:

Virtual void Attack();

In a child class, we can then override that function by providing our own definition, provided the signatures of the two functions are identical. This override is done automatically, however, C++11 introduced the override keyword to specifically denote where a function will override the function of a parent. The override keyword is optional, but it's considered good practice and it is recommended. It is used as follows:

Void Attack() override;

C++11 also introduced the final keyword. This keyword is used to designate virtual functions that cannot be overridden in a derived class. It can also be applied to classes that cannot be inherited. You can use the final keyword as follows:

Void Attack() final;

In this case, the Attack() function could not be overridden by inheriting classes.

Pure virtual functions

Virtual functions that we just covered allow a function to be optionally overridden by an inheriting class. The override is optional, as the parent class will provide a default implementation if one is not found in the child class.

A pure virtual function however does not provide a default implementation. Hence, it must be implemented by inheriting classes. Furthermore, if a class contains a pure virtual function, it becomes abstract. This means that it cannot be instantiated, only inheriting classes, providing they provide an implementation for the pure virtual function, can be. If a class inherits from an abstract class and does not provide an implementation for pure virtual functions, then that class becomes abstract too.

The syntax that is used to declare a pure virtual function is as follows:

Virtual void Attack() = 0;

In the example of the Weapon parent class, which is inherited by Sword, Axe and Wand, it would make sense to make Weapon an abstract class. We will never instantiate a Weapon object; its sole purpose is to provide a common interface to its children. Since each child class needs to have an Attack() function, it then makes sense to make the Attack() function in Weapon pure virtual, as we know that every child will implement it.

Pointers and object slicing

The last part of the polymorphism puzzle is the use of pointers. Consider the following two lines of code:

Weapon myWeapon = Sword();
Std::unique_ptr<Weapon> myWeapon = std::make_unique<Sword>();

In the first line, we are not using pointers; in the second one, we are. It is a seemingly small difference, but it produces extremely different results. To properly demonstrate this, we're going to look at a small program that defines a number of weapons.

Tip

If the Weapon class contains a pure virtual function, the first line of the preceding code won't be compiled since it will be abstract and cannot be instantiated.

You can download the code for this program from the Packt Publishing website. It will be in the Examples folder, and the project name is polymorphism_example:

#include <iostream>

// We're using namespace std here to avoid having to fully qualify everything with std::
using namespace std;

int main()
{

  // Here we define a base Weapon struct.
  // It provides a single data type, and a method to return it.
  struct Weapon
  {
    string itemType = "Generic Weapon";

    virtual string GetItemType()
    {
      return itemType;
    }
  };

  // Here we inherit from the generic Weapon struct to make a specific Sword struct.
  // We override the GetItemType() function to change the itemType variable before returning it.
  struct Sword : public Weapon
  {
    string GetItemType() override
    {
      itemType = "Sword";
      return itemType;
    }
  };


  Weapon myWeapon = Sword();

  // output the type of item that weapon is then wait.
  cout << myWeapon.GetItemType().c_str() << endl;
  std::cin.get();

  return 0;
}

In this code we created a base struct Weapon. We then inherit from it to create a specific implementation named Sword. The base Weapon struct defines the GetItemType() function and Sword overrides it to change and then return the item type. This is a pretty straightforward case of inheritance and polymorphism, but there are some important things that we need to know that could otherwise trip us up.

As the code currently stands, the Weapon object is instantiated in the following way:

Weapon myWeapon = Sword()

Let's run the code and see what we get:

Even though we assigned myWeapon a Sword object, it's a Weapon object. What's happening here? The problem is that myWeapon is given a fixed type of weapon. When we try to assign it a Sword object, it gets passed to the copy constructor of Weapon and gets sliced, leaving just a Weapon object. As a result, when we call the GetItemType() function, we call the function in Weapon.

Tip

For a more in-depth explanation of object slicing, head to http://www.bogotobogo.com/cplusplus/slicing.php.

To avoid this and make good use of polymorphism, we need to work with pointers. Let's make the following change to the code:

  // Create our weapon object.
  //Weapon myWeapon = Sword();
 std::unique_ptr<Weapon> myWeapon = std::make_unique<Sword>();

Tip

Smart pointers such as unique_ptr require the include <memory>. So don't forget to add this to the top of the file.

Since we've now changed myWeapon to a pointer, we also need to change the following:

// Output the type of item that weapon is then wait.
//cout << myWeapon.GetItemType().c_str() << endl;
cout << myWeapon->GetItemType().c_str() << endl;

When working with pointers, we need to use the -> operator to access its variables and functions. Now, let's rerun the code and see what the output is:

This time, we called the overridden function in the Sword struct as intended, and it boils down to the way we defined myWeapon.

Since myWeapon is now a pointer to a Weapon object, we avoid object slicing. Since Sword is derived from Weapon, pointing to a Sword in memory isn't a problem. They share a common interface, so we achieve this overriding behavior. Returning to the initial definition, polymorphism is the ability to access different objects through an individually implemented common interface.