Goodbye InheritanceSolution for a problem we inflicted upon ourselves |
Date | ||
---|---|---|---|
Updated | |||
Author | Fix | License | CC0 |
Object-Oriented Programming has been present in game development for a long time, and it’s not going away any time soon, no matter how much I wish it would. Chances are, I cannot convince you to fully give it up, but I’m going give an example from an OOP-less world, present a case for why you should use less inheritance in your code and offer a more flexible alternative.
Let’s say in your game you have 2 types of entities. One entity is a warrior, whose main role inflicting damage and guarding areas. Another entity is a builder, whose main role is constructing buildings. They both inherit from the same class, but the Builder cannot attack and Warrior cannot construct buildings.
Inherited by ┌─────────┐ ┌─────────────►│ Warrior │ ┌────────┐ │ └─────────┘ │ Person ├─┤ └────────┘ │ Inherited by ┌─────────┐ └─────────────►│ Builder │ └─────────┘
Now, let’s say, you wanted to introduce a Skirmisher, a unique unit that can attack and construct buildings. From whom should the Skirmisher inherit? If it inherits from the Warrior, you’ll have to duplicate the construction code and if you inherit from the Builder you have to duplicate attack code. In either case you maintain the same code twice. This is not end of the world, but can get complicated the more types of characters you add. This type of complexity is also very inflexible, with each new character type requring a lot of template and copy-paste work.
Inherited by ┌─────────┐ ┌─────────────►│ Warrior │ ┌────────┐ │ └─────────┘ │ Person ├─┤ └────────┘ │ Inherited by ┌─────────┐ └─────────────►│ Builder │ └─────────┘ ┌────────────┐ ?│ Skirmisher │ └────────────┘
If you’ve made even just one game while following OOP, you’ve encountered this situation, probably multiple times. But there is a very simple way to avoid this.
I’m only mentioning this because some developers I discussed this with brought this up as a first solution: Interfaces. They partially solve the inflexibility problem - if we ever needed a new entity type, we can simply inherit from the interfaces with desired behaviour.
┌────────────┐ ┌───────────►│ Warrior │ ┌──────────┐ │ └────────────┘ │ IWarrior ├─┤ └──────────┘ │ ┌────────────┐ └───────────►│ │ │ Skirmisher │ ┌────────────┤ │ ┌──────────┐ │ └────────────┘ │ IBuilder ├─┤ └──────────┘ │ ┌────────────┐ └───────────►│ Builder │ └────────────┘
Unfortunately, they are available only in some languages, for example, C# has them, but C++ does not (yay for inheritance hackery and vtables!). But they can be unwieldy when you don’t know what interfaces your entity is inheriting and even worse: it can be slow. Attempting to cast then checking against null can be computationally expensive, which is what we obviously want to avoid in a game that has to run at least at 60 frames per second. While this is a step in the right direction, but I have a more flexible and usually faster solution.
A solution that works for more languages is simple: enumerator bitflags used as tags. Think of our game loop as a data-processing programs, and the entities as the input. We have one object - Entity. Inside the Entity we define a variable to serve as our bitflags. To follow the previous example: the WARRIOR and BUILDER flags.
|
|
Later on, when we are processing our entities, we check for flags, and perform the desired behaviour. Coming up with new types of entities is easy - we just assign the combination of flags needed for desired behaviour. If we have new behaviour, we can add it into our loop.
|
A neat benefit of this approach is that we can modify behaviour during runtime. Let’s say we want to create an entity can be upgraded, it begins as a warrior and later learns to build. This is very easy!
|
As you can see, tags are very flexible, and due to the simple nature of bitflags, they can be pretty fast too!
You don’t have to use your flags to track just entity types. Entity
states such as ALIVE
or even whether it should be affected
by certain systems, like STATIC
or
PHYSICS_ENT
. You got up to 64 bits you can store in one
variable, use them as you wish!
Of course, there are many ways to improve upon this, architecture and performance-wise, but this is just a simple example to help you visualise the idea. As with everything, you should adapt how you use this principle to your specific case to maximise benefits.
I would like to thank Alex Reed, Duka and No Need, for helping me polish some of the rougher bits of the article!