Developers with an OOP background, no matter how experienced, often struggle when they first stumble upon components on GameObject
s in Unity. Alternatively, many enforce OOP principles in their Unity projects… and struggle later on.
While component-based design is easily implemented within an OOP environment, working with components requires a different approach, or at least benefits from it. Multiple systems within Unity’s engine and editor are designed with proper use of components in mind, and embracing component-based design will allow you to work with those systems, rather than against them.
This article is highlighting core ideas of OOP and then presents equivalent structures in Unity’s component-based design. Note: This article is not about Entity-Component-System (ECS).
What is inheritance?
Inheritance is one of the core concepts of OOP that some coders have a rather one-sided knowledge of. Inheritance can be explained in either a semantic way or a technical way, and understanding both sides is crucial.
To define inheritance on a technical level, we can say that we’re talking about a class that has all the properties and interaction options of another class. The “subclass” inherits these traits from the “superclass”. This allows us to use an instance of the subclass wherever an instance of the superclass is requested. If a “Car” can honk, then so does a “Firetruck” – if the Firetruck class inherits from the Car class.
The semantic sense behind inheritance is that human brains see inheritance everywhere in the real world. Inheritance is the coding implementation of an is-a relationship. A firetruck is-a car. This shows in natural language, as you can point at a firetruck and say “There’s a car!” without causing confusion.
In game development, as anywhere else, inheritance can be used to define classes of objects with similarities. A player can enter any type of vehicle – like cars and helicopters – without the developers having to define how “entering” works multiple times.
Games might have inheritance hierarchies like
CombatHelicopter < Helicopter < AirVehicle < Vehicle < Interactable < Entity
with every class in the chain adding a bit more functionality towards finally defining what a “CombatHelicopter
” is.
How could we use inheritance in Unity?
In Unity, we don’t create a “CombatHelicopter
” object as the only type of object we can add to scenes is “GameObject
” (with ECS entities being the same in this context). That GameObject
then gets some components assigned in order to fill a specific role in the game.
People who learned to use OOP might be tempted to see this as a little hurdle and just create their inheritance hierarchy on the component side. In the end, the CombatHelicopter
GameObject
would have a single component called CombatHelicopter
that inherits from the Helicopter
component class, and so on.
This is not a good choice for two reasons: For one, as mentioned, Unity is designed with components in mind and will sooner or later cause you trouble when following this path. And on top, inheritance has weaknesses that show especially in game development.
Regarding those weaknesses, imagine a specific CombatHelicopter
in your game that, for whatever reason, does not allow the player to get in. With the inheritance approach, you could add a property to the Vehicle class that prevents entering. That property would allow you to disable entering a Vehicle, and it is used by that CombatHelicopter
object. This means that you have to change the Vehicle class in order to change the functionality of a CombatHelicopter
. A developer in your team who didn’t work on the CombatHelicopter
class might ask you “why is this here?”, as it might look like unneeded clutter for that specific class to disable its entire functionality. Alternatively, you could override the Enter and Exit methods in your CombatHelicopter
class, which would not be a clean solution either – imagine wanting a specific override behavior, and perhaps wanting that for two kinds of vehicle classes.
What can we use instead?
Instead of creating an inheritance hierarchy within our component codebase, we can create the same components without having them inherit from each other. They all inherit directly from MonoBehaviour
, and since they don’t inherit each others contents, they’re all added to the GameObject
together.
In many cases, it turns out that the components don’t even need to know each other. They were related to the other classes in the inheritance hierarchy because they had to – if they weren’t, their functionality would be missing in the CombatHelicopter
class. However, the weapons the CombatHelicopter
class introduces are likely completely unrelated to the other functionalities of the object.
And that’s where the advantages of component-based design start. If you don’t want the player to be able to enter the vehicle, just disable or even remove the Vehicle
component. The object still has all other capabilities, even if you take something right from the middle of it.
And if you make another type of vehicle, like a car, you can use the “Weaponry” component on your car GameObject
to make it a tank. Transferring functionality like this isn’t as easy with inheritance, and interfaces don’t help a lot either, as they don’t (or shouldn’t) contain any implementations.
But what about dependencies?
Of course, if we stopped here, components instead of inheritance would seem rather unpleasant. What happens to the AirVehicle
component if we remove Vehicle
? And how do they even interact when they clearly depend on each other?
Without any lengthy introduction, let me show you this pattern:
[RequireComponent(typeof(Vehicle))] public class AirVehicle : MonoBehaviour { private Vehicle vehicle; private Awake() { vehicle = GetComponent<Vehicle>(); } }
The AirVehicle
component caches a reference to the Vehicle component in Awake. This is a very common pattern and should not be too new to any Unity developer. Slightly less known is the RequireComponent attribute. It marks a component as depending on another component type, and with that, it announces that it uses the specified component’s properties and/or methods. It’s Unity’s component-based equivalent to inheritance.
[RequireComponent]
does two things. When component A
requires component B
,
- when you add an
A
component to aGameObject
that does not have aB
component, Unity will add it if possible. If it can’t add it, theA
component will not be added either. This applies as well when you callAddComponent<A>()
. - if you try to delete a
B
component while anA
component is on the sameGameObject
, Unity will not allow it.
Unity prevents component deletion both in the editor and at runtime if you use Destroy()
.
Note that, in older Unity versions, adding this attribute to your component class does not change any GameObject
s you already created. If you add a component and then add the attribute, you can get in trouble. Newer Unity versions actually update your prefabs when you add the attribute to a component that’s on them. Neat!
Using this pattern, you can have one component access the functionality of another. When using inheritance, you’d call a superclass method; now, you just call it using the cached reference.
What about template methods?
Inheritance goes hand in hand with polymorphism, which is the concept of having subclasses implementing or overriding the behavior declared in the superclass. You could create an abstract Car class, and then create subclasses for automatic and manual transmission cars. They’re both cars, but they have fundamental differences in the way they work.
A template method is a method that isn’t implemented in the superclass, so subclasses have to implement it. In our example, the AirVehicle
probably massively changes the movement behavior of a GameObject
compared to a GameObject
that only has the Vehicle
component. How can we make the Vehicle
component allow changes to its behavior depending on whether or not the seemingly unrelated component AirVehicle
is present?
The answer is: With the observer pattern. This pattern is used in OOP for many purposes, and can help here as well. Let’s implement it here.
Let’s assume that the Vehicle
component handles the player’s input as long as the player character is inside the vehicle. Perhaps, it handles input from an AI class if an NPC is controlling the vehicle. In this case, it would perhaps read input data in its Update method:
private void Update() { var input = GetInputDataFromSomewhere(); // Do something with that input here }
The input data could be a struct containing all relevant inputs: Acceleration, turn direction or whether a weapon fire button is pressed. Let’s call it “MyInputStruct
“.
Since the Vehicle
class is rather abstract – it represents neither a car nor a helicopter – it won’t handle the inputs itself. Instead, add a field for a listener who can handle the input.
private Action<MyInputScruct> handleInput; public void RegisterInputHandler(Action<MyInputScruct> handleInput) { this.handleInput = handleInput; }
Now, we can extend the Update
method:
private void Update() { if (handleInput != null) { var input = GetInputDataFromSomewhere(); handleInput(input); } }
And finally, we extend the Awake
method in the AirVehicle
component:
private Awake() { vehicle = GetComponent<Vehicle>(); vehicle.RegisterInputHandler(HandleInput); } private void HandleInput(MyInputStruct input) { // Fly around according to the input }
Now we have not only a component that requires another component to be present on the same GameObject
– it also implements that component’s missing behavior by passing it a method to call. With this, we have implemented the equivalent of a template method.
You can easily extend this to have a list of methods (or an event Action
) and multiple components registering into that. That way, multiple components can depend on one other component and implement its behavior together.
Conclusion
As a programmer with an OOP background, using components in a flat hierarchy can be counter-intuitive at first. However, you can benefit a lot from embracing the concept. Imagine your level design team slotting components onto GameObject
to achieve just the funcionality they want without having to ask the coding team for assistance all the time. Once your mindset includes proper component-based design, the way you design components as modular plugins to an object’s behavior will benefit your project greatly.
Start with the rule of thumb:
All my components inherit directly from MonoBehaviour
.