From the previous post you would’ve understood that declaring a pure virtual function converts a class to an abstract class. This opens numerous doors and gives us infinite options for structuring our project. We can get rid of cluttered or messed up code flow and enforce elegance to the code!
In games, we need various systems independent of each other to interact/converge in one or more instances. GamePhysics; has nothing to do with rendering, it has to bother about the movement and collision of various objects. Should it interact with GameRenderer? Yes, it has to, at least when the game is under development, we should be able to visualize the bounding box, the dir vectors etc of the objects. Thus, we need these two unrelated systems to interact. Likewise, there is no independent system in a game. This is true for almost all systems. Everything has to interact with everything else. There are very few instances where a system can work in complete isolation.
Due to this requirement, we should enforce certain rules so that the classes we create can adapt themselves when these interactions happen. Let’s say we have derived all game characters from a common base class Actor
. What rule can we enforce on these characters to ensure that all game characters can be rendered?
Abstract class
We know that abstract classes can enforce a set of rules in our programs. To convert a class to an abstract class, we need only one pure virtual function. Even if 9 of 10 functions are defined, the one pure virtual function converts it to an abstract class. This can be utilized effectively to enforce rules and create traits and policies for various elements in our game. This can make the code super flexible and removes code dependencies across different parts of the game code.
Traits
These rules that we enforce are called traits. Identifying traits in our system and enforcing them plays a vital role in utilizing this pattern efficiently. Let’s consider we want to enforce render as a rule to Actor
class, we create a Renderable
abstract class and implement it in the Actor
.
class Renderable {
virtual void render() = 0;
};
class Actor : public Renderable {
// since Actor is 'derived' from Renderable,
// it has to implement the pure virtual function render
virtual void update() {
//do actor update
}
virtual void render() override {
//do actor render
}
};
Consider, we should make sure Actor
is having a couple of physics properties, a collision property. We will create an abstract class Collidable
that will enforce certain rules on these collision objects.
class Renderable {
virtual void render() = 0;
};
class Collidable {
virtual void onCollision() = 0;
virtual void onEnterCollision() = 0;
virtual void onExitCollision() = 0;
}
class Actor : public Renderable, Collidable {
// since Actor is 'derived' from Renderable,
// it has to implement the pure virtual function render
virtual void update() {
//do actor update
}
virtual void render() override {
//do actor render
}
virtual void onCollision() override {
//do actor on collision
}
virtual void onEnterCollision() override {
//do actor enter collision
}
virtual void onExitCollision() override {
//do actor on exit colliison
}
};
Now, we see that the Actor
is both a Renderable
and a Collidable
. This means, all derived types of the Actor
can be referred to with these two base types Renderable
and Collidable
based on the requirement. Suppose, if we are having multiple systems, we can do this
class Monster : public Actor {
/// monster stuff
};
class Player : public Actor {
/// player stuff
};
void main() {
std::vector<Actor*> allActors;
std::vector<Collidable*> allCollidables;
std::vector<Renderable*> allRenderables;
Monster* m = new Monster;
Player* p = new Player;
allActors.push_back(m);
allActors.push_back(p);
allCollidables.push_back(m);
allCollidables.push_back(p);
allRenderables.push_back(m);
allRenderables.push_back(p);
}
The advantage of enforcing a rule as a trait is; there is no requirement for a direct dependency between two items. Any type can have any property and not have any other property, most of the time. There is a degree of flexibility here when compared to the traditional base and derived class terminology.
Referencing using Trait
It may look like we are doing redundant stuff; which is not the case. There is much more stuff that is not an Actor
but will be Renderable
and Collidable
, for example, a tree
in the environment and the skybox
are Renderable
but not actors. A Weapon
and a Shield
are not Actors
but are Collidable
and Renderable
. An invisible TriggerArea
is a Collidable
but is neither a Renderable
nor an Actor
. Likewise, numerous combinations of items and rules can be enforced on various classes.
class Monster : public Actor {
/// monster stuff
};
class Player : public Actor {
/// player stuff
};
class StaticProps : public Renderable, Collidable {
/// environment stuff
}
class Tree : public StaticProps, Animatable {
/// tree stuff
}
class Rock : public StaticProps {
/// rock stuff
}
class Coin : public Renderable, Collidable, Collectable {
/// coin stuff
}
As it is shown in the example code, allRenderables
can hold any type that will implement the trait Renderable
. It doesn’t matter if they are derived from the same base class. In this context, they are derived from Renderable
and hence Renderable
is the base class. The same rule is applied to a Collidable
, Animatable
, Collectable
etc. The idea is, we can refer to all the types under the trait as a reference to the trait itself. Traits pattern helps us in referencing and managing multiple unrelated types as the same type and processing them as a particular system.
Was this series on virtual functions useful? Have any questions? Did I make any mistakes? Let me know on Twitter or Mastodon!
Tags
- CPP
- virtual function