Third in a series of posts on Software Design for Unity. Read the introduction here.
Inheritance is a big deal in object oriented programming. The ability to extend parent classes into subclasses, and have those subclasses be recognised as the parent (via polymorphism), is extremely powerful. It’s one of those Big Ideas that, once it clicks in your head, drastically changes how you think about coding.
And if you’re not careful, it ends up making your life far more difficult than it should be. Because it’s also one of those hammers that makes everything look like a nail. Inheritance allows – and encourages – one to structure code in hierarchies. But the fact is, not everything is a nail hierarchy.
If you buy into the idea that everything should be solved using inheritance, you’re going to spend a lot of time and effort trying to make non-hierarchical structures look like hierarchical structures. Not only that, but the design of the codebase will usually end up worse than if you hadn’t – harder to understand, maintain, extend etc etc.
Fortunately, one of the absolute best things[1]in my opinion, as always about Unity is its widespread use of composition. While inheritance and composition aren’t mutually exclusive (in fact, they’re extremely powerful when used together correctly), they essentially represent two opposing ways of thinking about any specific problem. Composition is pretty awesome, and I’ll talk about it later. But first, let’s look at the core pitfall with inheritance. So, back to the idea that:
Not Everything is a Hierarchy
Imagine you’re creating a simple bullet.
public class Bullet : MonoBehaviour { public void Fire(){ // Start moving } public void Update(){ // Move along path } private void OnCollisionEnter(){ // Check if we’ve hit an enemy // If so, damage it // Either way, destroy self } }
Some time later, you want to make a different type of bullet – a bullet that explodes on impact. Simple – the only difference here is that it explodes, so you inherit from Bullet to get “for free” the basic bullet behaviour, and only have to write one new method.
public class ExplodingBullet : Bullet { private void OnDestroy(){ // BOOM } }
Lovely. Now, a little later, you want to make a homing bullet. Ok, so do the same again, but instead modify the Update method.
public class HomingBullet : Bullet { // Assume we’ve changed Update() in Bullet to be protected virtual protected override void Update(){ // Rotate towards target // Move } }
So far so good. You’ve reused code, and any other code that deals with Bullet will recognise both of these subclasses as a Bullet. This is the Power Of Inheritance at work. The obvious next step? Making an exploding homing bullet!
… and then it all goes a bit pear-shaped.
Your first attempt is to make HomingBullet inherit from ExplodingBullet instead of Bullet. But now all HomingBullets are explosive, which you don’t want. So switch it around – ExplodingBullet inherits from HomingBullet. Same problem – now all ExplodingBullets are homing.
You can, of course, put a boolean in to specify whether these attributes should actually be used in a particular instance. But in that case, why have separate ExplodingBullet and HomingBullet subclasses at all? You put the homing and exploding behaviours back into Bullet and have a set of bools to define the behaviour. This is ok, but then inheritance has proved to be totally useless after all. Now imagine you’ve got a bunch of other attributes a bullet could also have – suddenly your Bullet class is getting pretty messy!
The concepts of explodingness and homingness do not have a parent-child relationship with each other
Here’s the problem summed up – the concepts of explodingness and homingness[2]highly accurate, technical terms I just made up now do not have a parent-child relationship with each other. Bullets can be one, both or neither, so these attributes are not above, below or even alongside each other. They’re totally independent. So trying to cram them into a hierarchy just isn’t going to work naturally.
Here’s where composition comes in.
Composition And You(nity)
Look back up at that simple Bullet class code above. Like a lot of the code you’ll write in Unity, it inherits from MonoBehaviour. What you may not know is that MonoBehaviour itself inherits from another Unity class – Component[3]In actual fact, Component is a couple of levels above MonoBehaviour, but the point stands.. That name isn’t a coincidence. This is Unity’s composition system, and (stay with me) it’s one of those times where inheritance has been used really well – by inheriting from MonoBehaviour, you get access to a really powerful set of composition tools by default.
Composition is the idea that objects can be put together bit by bit, instead of inheriting wholesale from a hierarchy. One of the holy grails of object orientation has for a long time been multiple inheritance – the idea that classes can inherit from multiple different parents instead of just one. In practice, this has proved almost impossible to do “correctly”, and very few languages support it (C#, for example, does not[4]although some might argue that interfaces are an extremely limited form of multiple inheritance.). Those that do all have a different way of doing it.
One could easily argue that the whole venture is flawed, and that a compositional model solves the same problems without all the headaches. A frequently used maxim is that inheritance gives things “Is A” relationships, whereas composition gives things “Has A” relationships. The first is very strict; something that “is” something else must adhere to all its parent’s constraints. The second is not – if something “has” something else, it can also “have” lots of other things. This allows you to cherry-pick attributes to compose an object with the desired behaviour.
Under this model, homingness and explodingness, since they are independent attributes, are behaviours that can be coded into separate components. These components can be combined with the Bullet class to modify its basic behaviour. In Unity, this is done by simply adding any combination of the three MonoBehaviours to a GameObject in the editor!
public class Exploder : MonoBehaviour { private void OnDestroy(){ // KABLAMMO } } public class Homer : MonoBehaviour { private void Update(){ // Rotate towards target } }
This has other benefits, particularly when viewed in terms of encapsulation. Each component has a single responsibility, and need not know anything about the others. The Bullet component doesn’t need to know that it’s going to explode when it hits something, or that it’s going to home in on an enemy. It just needs to move forward and do damage if it hits an enemy. Likewise the Exploder and Homer components don’t need to know they’re attached to a Bullet, or communicate with it. And you can add any number of attributes in the future by creating new components, without having to rewrite existing components to account for them. Furthermore, these behaviours can be added to things other than Bullet, if so desired.
Of course in real life, components do often need to talk to each other. Again, Unity has you covered, with methods such as GetComponent<>(), and (somewhat differently) attributes such as [RequireComponent()]. Being built from the ground up around this type of architecture, composition is often the easiest and best way to do things in Unity.
Conclusion
Hopefully it should be fairly obvious that composition allows you to create a highly modular, encapsulated codebase which can be extended and reused in a really flexible way. Unity’s component model is great, and it’s worth trying to work with it rather than against it. But as I’ve said before, no one technique is the silver bullet that will make all your code awesome, and there are plenty of cases where composition isn’t the best tool to use. Likewise, inheritance is a hugely useful tool when used correctly. The only problem is the (seemingly widespread) tendency to see everything in terms of inheritance.
Inheritance is great for generalising, whereas composition is great for specialising
In my experience, a good general rule of thumb is that inheritance is great for generalising, whereas composition is great for specialising. If you’ve got something you need almost all of your code to do, it’s often a good idea to create a base class which implements frequently-used functionality, framework or glue code[5]It’s also worth noting that not all your code needs to, or should, inherit from MonoBehaviour – if it doesn’t make sense as an object “in” the game world, you’re often better off with vanilla classes. But that’s a whole other topic.. This is exactly what MonoBehaviour does, to hook you into the component system and so forth. But if you’re coding gameplay-level behaviour that may or may not be used by specific things, compositional thinking can be exactly what you need.
Footnotes
1. | ↑ | in my opinion, as always |
2. | ↑ | highly accurate, technical terms I just made up now |
3. | ↑ | In actual fact, Component is a couple of levels above MonoBehaviour, but the point stands. |
4. | ↑ | although some might argue that interfaces are an extremely limited form of multiple inheritance. |
5. | ↑ | It’s also worth noting that not all your code needs to, or should, inherit from MonoBehaviour – if it doesn’t make sense as an object “in” the game world, you’re often better off with vanilla classes. But that’s a whole other topic. |
13 Comments-
-
-
-
-
-
-
-
-
-
-
-
-