As a frequent reader of the gamedev.net game development forums, I often notice that most beginner and intermediate developers start designing the architecture of their game by deciding on a base class from which all game objects will inherit, and storing a list of these objects. The justifications given are usually that:
- It’s Object Oriented.
- Inheriting from a base class allows code reuse.
The most appealing advantage of the “base class everything inherits from” approach is that it provides the illusion of a solid design foundation (that is essential to the “getting things done” feeling that motivates many of us) without requiring a lot of thought. All that is to be done is to slap together a few functions into a base class (usually plagiarized from one of the many tutorials available on the web) and then start inheriting.
The Issues
The naive approach to the base class solution can be summarized as this:
interface BaseObject { void render(); void update(); }
In theory, this interface seems to do everything required: after all, every object will have to update its state, and every frame it shall render itself to the screen. In practice, however, this approach leads to two serious issues:
- Not all objects are both updated and rendered. For instance, a script or a trigger cannot be rendered, but they still have to be regularly updated. The least that could be done here is to separate the rendering side of things from the update side.
- Sometimes, updating requires access to other objects. This happens for instance when objects collide or when an AI looks for a target. However, interacting with other objects only happens through the base object interface, which provides no information about what the other object is. Even assuming that all objects have a collision hull as part of their public interface (which in itself also does not make any sense, since many objects do not collide with anything), determining that the other object in a collision is a friend or an enemy requires knowledge beyond any acceptable base interface.
While the former can easily be solved by using two base classes instead of one, the latter is much more problematic. It is usually solved through contraptions such as downcasting (which eliminates the benefits of the Liskov Substitution Principle) or the Visitor Design Pattern (which eliminates the benefits of the Open-Closed Principle in most modern static languages).
The benefits
But surely people wouldn’t be using base classes this way if there wasn’t a tangible benefit to using them, would they? Well-known games (including the original Half Life engine, which was mod-friendly enough to give birth to a vast number of mods) have used this approach through the ages.
There is a fundamental rule for interface implementation and class inheritance:
The finality of every interface and every inheritance relationship is to allow code to operate on different types of objects.
The sole benefit of using a base class for game objects is to store those objects in a single container and apply update and render functions over that container. Everything else in a typical game requires using a more specific type through visitation or downcasting.
When, then, is a single container useful? In the case of a game engine, a single container is useful because on the one hand, it’s preferable to have the engine handle the storage, lifetime and processing of game objects and on the other hand, designing a game engine to allow the addition of other containers (one for each type of object, for instance) is difficult.
As such, having everything inherit from a single type makes communications between the engine and the game much easier (only one type of object has to be carried through the interface), even though it might make the development of the actual game harder.
Of course, a container may allow multiple interfaces to exist: a container for renderable objects, a container for updated objects, a container for objects with collision detection, a container for objects with a physical model, a container for objects which are synchronized over the network and so on. However, the original problem remains: when two objects collide, the collision response depends on the exact type of the objects, which exists in game space but not in engine space. The engine is therefore by construction unable to provide the original most derived types back to the game, and so the game must retrieve them by other means (note that visitation is not possible, because it requires to encode the possible most derived types in the visitor, which itself is encoded in the interface of the visited object.
However, beginner and intermediate programmers don’t write engines (or at least, they shouldn’t, because the motive that drives studios to write engines is the necessity to distribute programming tasks across large teams with varying competence, and to keep the code for later projects), they write games. The source code of these games is worked on by only a small handful of programmers, usually only one, which allows a far greater level of control, especially for purposes of refactoring. As such, the necessity of having generic containers disappears, and the main benefit of a single base class with it.
Segregate types
My advice is pretty simple: strive to keep one list per type of object. In your average video game, you would have a list of projectiles flying around, a list of AI-controlled opponents, and a player-controlled character.
Do not let this approach hurt code reuse, however. If both the player character and the AI-controller opponents can take damage from walking in lava, you don’t want to write the damage assignment code twice. Instead, you can have both the AI-controller opponents and the player-controlled character implement the same interface with a single “takeDamage()” function, and apply the damage assignment code to both. You may even share the health/damage handling code between the player and the opponents by aggregating a “health” class, by inheriting from a “Character” class which manages health, or by applying a “WithHealth” mix-in:
class IDamageTaker { public: virtual void takeDamage(int); }; template <typename T> class WithHealth : public IDamageTaker, public T { int health; public: void takeDamage(int d) { health -= d; } void healDamage(int d) { health += d; } bool alive() const { return health > 0; } }; class Vanilla { public: virtual ~Vanilla() {} }; class PlayerCharacter : public WithHealth< WithPosition<Vanilla> > { /* Player-related data here */ }; class AIOpponent : public WithHealth< WithPosition< WithTarget<Vanilla> > > { /* Opponent-related data here */ };
Now, the code for health management is not duplicated and the code for assigning damage is not duplicated. The only dupicated code is the traversal of the various lists (since instead of traversing a single list, now several lists must be traversed in order to reach all elements), and that code is not only very small, but also fairly easy to factor out (for instance, by implementing an iterator that traverses several containers in order).
On the other hand, because of the presence of several lists, there is no more need for determining whether a projectile has hit an opponent or the player: the type of the object is known simply because the currently processed list is also known.
Hi. I'm Victor Nicollet,
0 Responses to “Segregation is Good”