Game Object Structure: Inheritance vs. Aggregation
By Kyle Wilson
Wednesday, July 03, 2002Every game engine I've encountered has had some notion of a game object, or a scene object, or an entity. A game object may be an animated creature, or an invisible box that sets off a trigger when entered, or an invisible hardpoint to which a missile attaches. In general, though, there will be a game object class which exposes an interface, or multiple interfaces, to the systems that handle collision, rendering, triggering, sound, etc.
At Interactive Magic, and later at HeadSpin/Cyan, I worked with inheritance-based game objects. There was a base game object class, from which were derived other object types. For example, dynamic objects derived from game objects, and animated objects derived from dynamic objects, and so forth. When I got to iROCK, I found a similar scheme, but with some differences that I'll discuss in a minute.
There are several problems with a game object design based on inheritance. Some problems can be worked around (with varying degrees of difficulty). Some problems are inherent in an inheritance-based design.
- The Diamond of Death. What do you do if trigger objects are derived from game objects, and dynamic objects are derived from game objects, but you want a dynamic trigger object? In C++, you have to make your trigger objects and dynamic objects inherit from game objects virtually. Virtual inheritance is poorly understood by most programmers, makes maintenance more difficult, and adds subtle inefficiencies to your code. (Read [7] for details. Also see [6], Item 43, for other problems with multiple inheritance.) Inheritance handles overlapping categories poorly.
- Pass-Thru Enforcement. In a game object hierarchy -- more than most class hierarchies, I think -- it's useful to have functions perform class specific actions, then pass the function call down to a parent class. For example, at HeadSpin, our game object base class declared a virtual function called Update. Update was intended to do everything a class required before being drawn. In the root game object class, it refreshed the current world space transform for the game object. In derived classes, Update might change other state settings, then call Update in the parent class. But C++ doesn't offer any convenient mechanism for requiring that a virtual function recurse down through base class implementations of itself. Every time we added a new class to the engine, at least one function pass-thru got left out, and had to be caught in debugging.
This problem can be ameliorated somewhat if you don't publicly expose virtual functions, but instead use the Template Method pattern[3], or what Herb Sutter calls the "Nonvirtual Interface idiom"[8]. That is, have a base class consisting only of public non-virtual functions which call private virtual functions. The non-virtual functions can perform whatever base-class-specific actions they require before calling the private virtuals. This still won't help, however, with deep class hierarchies where leaf classes need to pass through function calls to intermediate parent classes, not just the root class.- Unintended Consequences. A corollary to the Pass-Thru Enforcement problem is that when a virtual function call recurses down the class hierarchy, it's easy to lose track of which actions are being performed for which classes. In [5], Herb Marselas writes that since Ensemble used an inheritance-based game object hierarchy for Age of Empires II, "functionality can be added or changed in a single place in the code to affect many different game units. One such change inadvertently added line-of-sight checking for trees," which was a performance problem, since an AOE2 level generally contained a large number of trees.
- Dependencies. Inheritance is one of the tightest couplings there is in C++. As such, it affects not just program logic, but also the physical design of a program. Inheritance always creates compile- and link-time dependencies in your code. To compile a file using any game object, the parser must also load the header files containing all its ancestors. The linker must resolve all dependencies to the same. If any game object needs to know about its descendents, then cyclic dependencies arise, and proper levelization becomes impossible. (See [4].)
- Difficulty in Comprehension. To understand the behavior of any class, you have to open other files and learn the behavior of its ancestors.
- Interface Bloat. Monolithic classes tend to develop large interfaces to cover the host of purposes they serve. By the time I left Cyan, our scene object base class had a class definition alone that was over four hundred lines of flag enums and function declarations for physics, graphics, sound, animation and stream I/O. And the interface would have been bloated further by null virtual functions used by derived classes if we hadn't instead resorted sometimes to just checking type-ids and doing static_casts to derived types.
We partly solved this problem at iROCK by having multiple class hierarchies, instead of just one. What would normally be one game object becomes three, a game object, a scene object, and a sound object, all in separate modules. This is an improvement in principle, but in practice, independence of the different modules was never well enforced. In the end, game objects included scene object headers, sound objects included game object headers, and the code was rife with the cyclic dependencies that John Lakos so deplores.So if an inheritance-based game object design is riddled with problems, what is a better approach? In redesigning the Plasma engine used by HeadSpin/Cyan, the software engineering team opted to flatten the game object hierarchy down to a single game object class. This class aggregated some number of components which supported functionality previously supported by derived game objects.
In a post to Sweng-Gamedev back in early 2001[1] -- around the time Cyan started rearchitecting Plasma -- Scott Bilas of Gas Powered Games described changing the Dungeon Siege engine from a "static class hierarchy" to a "component based" design. In the same thread[2], Charles Bloom of Oddworld described the Munch's Oddysee class design as being similar to that of Dungeon Siege.
From these data points, I'm willing to interpolate a trend. If moving from class hierarchies to containers of components for game objects is a trend in game development, it mirrors a broader shift in the C++ development community at large. The traditional game object hierarchy sounds like what Herb Sutter characterizes as "mid-1990s-style inheritance-heavy design" [8]. (Sutter goes on to warn against overuse of inheritance.) In Design Patterns, the gang of four recommends only two principles for object oriented design: (1) program to an interface and not to an implementation and (2) favor object composition over class inheritance [3].
Component-based game objects are cleaner and more powerful than game objects based on inheritance. Components allow for better encapsulation of functionality. And components are inherently dynamic -- they give you the power to change at runtime state which could only be changed at compile time under an inheritance-based design. The use of inheritance in implementing game object functionality is attractive, but eventually limiting. A component-based design is to be preferred.
[1] Scott Bilas. Post to Sweng-Gamedev. Feb. 14, 2001.
[2] Charles Bloom. Post to Sweng-Gamedev. Feb. 15, 2001.
[3] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.
[4] John Lakos. Large Scale C++ Software Design. Addison-Wesley, 1996.
[5] Herb Marselas, "Profiling, Data Analysis, Scalability, and Magic Numbers, Part 2: Using Scalable Features and Conquering the Seven Deadly Performance Sins." Game Developer, Jul. 2000.
[6] Scott Meyers, Effective C++, Second Edition. Addison Wesley Longman, 1998.
[7] Stan Lippman, Inside the C++ Object Model. Addison-Wesley, 1996.
[8] Herb Sutter, Usenet post to comp.lang.c++.moderated. Aug. 22, 2001.
I'm Kyle Wilson. I've worked in the game industry since I got out of grad school in 1997. Any opinions expressed herein are in no way representative of those of my employers.