Inheriting Pain

Most of my development effort on Sanguinite lately has been spent refactoring a sizeable chunk of code having to do with the NPC/Enemy system. Anyone who’s built an app with a large, complex inheritance hierarchy probably knows how much pain that can cause down the line. It was a lot of tedious work that could have been avoided if Past Wheffle was a little bit smarter and more careful.


The Problem With Inheritance

Inheritance is an object-oriented programming design pattern that can help reduce redundant code. However, inheritance is rather rigid and it’s easy to paint yourself into a corner when building a large hierarchy.

For example, in Sanguinite I assumed every enemy in the game was going to be making use of the physics system. The top level object was a RigidBody type. Later it occurred to me that I may want to add enemies that ignore solid barriers or use some other method for interacting with the world that doesn’t burden the physics system (using physics can be expensive!)

Hierarchy trees get a lot bigger than this, but you get the idea.

However, interaction with the physics system was now deeply embedded in the highest level of NPC code. If I wanted to create an enemy that didn’t use the physics system, I’d either have to write a lot of nasty special case code, create a completely new type outside of the hierarchy causing a bunch of copy-pasted code and special considerations, or add a new top-level NPC type that didn’t use physics and reformat the whole tree. All of these solutions are gross.


The Nice Thing About Composition

Strict inheritance trees are generally frowned upon now in the programming community in large part due to the sorts of issues I had. I knew this, and yet made a poor decision when the project was young. “It’ll be fine,” said Past Wheffle. Why is Past Me so dumb so often?

The preferred alternate design pattern (barring any new Design Pattern Hotness I haven’t heard about yet) is to use composition. Instead of building a strict hierarchy tree where each object inherits from a parent, objects instead are composed of smaller bite-sized objects that are in charge of certain behaviors. You can attach as many of these components to an object as you want to form its behavior, like labels or tags.

Throwing a little inheritance in there anyway because I haven’t learned my lesson.

This is a much more flexible and “flat” system. Even if every NPC up to this point uses a “PhysicsBody” component, nothing stops me from creating a new NPC that doesn’t use that. All behavior can be broken out into atomic components and everything is nice and clean and happy and swappable. And thankfully, popular engines like Unity and the engine Sanguinite is being built on, Godot, encourage the use of composition and make it easy.

Obviously you can mix these systems. Maybe a very dedicated Cult of Composition guy could completely avoid using inheritance while building their application, but inheritance is still important and has a place. At least in my opinion. Both inheritance and composition are largely object-oriented programming design patterns and the effectiveness of OOP vs other modalities is still hotly debated by lots of angry pasty-skinned people, like myself. It’s quite a rabbit hole.


Plan For The Unplannable

Even if you design your application very carefully before your first line of code, most of the time you aren’t going to be able to account for all corner cases or features you end up wanting or needing to add in later. I could have foreseen this specific issue in Santuinite and structured my inheritance tree to accommodate, but you can’t count on clairvoyance. That’s why it’s important to try to keep your code as extensible as possible. Having to add an object into an inheritance hierarchy that doesn’t fit can cause a huge cascade of changes, which just sucks. Composition gives you a lot of extensibility with very little drawback.

The Big Refactor is done now and Sanguinite’s NPC system more heavily leans on a component system. However, I know I’ll be seeing bugs and odd behavior well into the future due to the shear amount of code that had to be altered. Experiences like this are nearly inevitable on sizeable projects, it’s par for the course, but hopefully we cause less and less of them as time goes on and we grow as programmers and architects.

One Comment

Comments are closed.