Unity3D Architecture – Understanding the Single Responsibility Principal
Unity3D architecture is something that doesn’t get nearly enough attention. With most other software disciplines, there are standard ways of doing things that have grown and improved over time. The goal of this article is to help bring one of the key principals of software to Unity3D developers and show how it can help improve your projects and make you smile when you look at the code.
The single responsibility principle states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.
Robert C. Martin expresses the principle as follows:
"A class should have only one reason to change."
What does this mean, and how does it apply to Unity game development?
To summarize, it means that when you create a class, it should do only what’s required to meet it’s single responsibility.
This applies to MonoBehaviours and plain old classes.
If you create a component for one of your prefabs, that component shouldn’t be responsible for more than a single thing.
Example: If you have a weapon class, it should know nothing about the UI system. Inversely, a WeaponAmmoUI class shouldn’t need to know anything about how weapons work, and should instead ONLY work on the UI.
Reading that, you may think “if each class only does one thing, there are gonna be a lot of classes”.
CORRECT!
If you follow SRP, you’ll end up with a large number of very small classes. While that may seem strange at first, it actually gives you a huge benefit.
Consider the alternative. You could have a very small number of giant classes. Or you could even go to an extreme and just have one mega class that runs your entire game (I’ve seen this attempted before, it’s scary).
Skeptical?
Before I go into details of the benefits and how to integrate the SRP into your process, let me point out a very prominent example of SRP in your existing projects.
Take a look at the built in Unity components. Look at the AudioSource component. It has one responsibility, to play audio. Audio isn’t played through a more general ‘entity’, ‘npc’, ‘random other abstract name’. It plays through an AudioSource.
The same goes for a Renderer component, a Transform, a RigidBody, and any other component. They each do one thing. They do that thing well. And complex behaviors often involve interaction between these components.
This is because the Unity team understands the benefits of SRP.
Benefits
Splitting up your logic into classes specifically responsible for one thing provides many great benefits:
- Readability – Classes are easy to keep between 20-100 lines when they correctly follow SRP.
- Extensibility – Small classes are easy to inherit from, modify, or replace.
- Re-usability – If your class does one thing and does that thing well, it can do that thing for other parts of your game.
Example: (HP Bars)
Imagine your game has the very typical need of HP bars over your NPCs heads.
You could have a base NPC class that handles all things NPC including the HP bar.
Fast forward a few weeks and imagine you get a new requirement and need to put HP bars over some buildings that aren’t NPCs.
Now, you’re in the disastrous situation where you need to extract all that HP bar code out into something you can re-use, or even worse you end up copy/pasting the HP bar code from your NPC class to your Building class.
Let’s see how that looks in an actual project and how to fix it.
Here, we have an NPC class that handles taking damage and death, and also does some UI work.
This is a super simple version of an NPC to avoid overwhelming the post with needless extra code.
When you look at this class, take notice at the # of things it’s doing.
- Managing Health
- Handling death
- Updating the UI
So this simplified NPC is already doing 3 things.
But we need more stuff, like particles when our npc dies!
Now, we’re doing 4 things… and it will of course explode into 10 or 20 things as the project continues. Logic will get more complex. The file will grow… and soon you’ll be in the soul sucking hell that is a 5000 line class.
I’ve seen plenty 10-20k classes as well, and even a 10k method.
Let’s take it apart!
We need to take this class apart piece by piece. For no particular reason, let’s start with the UI.
First, we’ll create a new class called HPBar.cs
This class will handle the HP Bar updating. Right now, if it looks like a bit of overkill, wait until we need to extend it.
To make this work, we also need to update the NPC class. HPBar.cs is looking for an OnHPPctChanged event to tell it when the UI should change.
What have we gained so far?
At this point, we’ve separated a tiny part of a small class off into something else. We’re doing it for a good reason though. We know our projects grow, and we know that our UI components for HP are going to be more complex than a slider. We’ll probably need to add floating HP text, maybe some numbers. We might need to make the bars flash when stuff gets hit. What we know for sure is that our HP UI system will grow, and now when we grow it, we don’t have to touch the NPC class at all. Everything we need to do is nice and isolated.
Keep splitting!
Okay, we cut one part off, it’s time to move onto the next. Let’s separate out the particle playing into an NPCParticles.cs class.
Our NPC.cs file needs to update as well… take a look though and see if you notice anything.
It’s shrinking!!!!!
Let’s take this even further and see what happens…
Create another file named Health.cs
Now we’ll update the NPC.cs file again.
We’ll also need to update the HPBar to look at Health instead of NPC.
And our particles also need to reference Health instead of NPC.
Cool it’s all split up… what now?
So far, I’ve shown you how to split the code up, but for this to stick, I want to show you some of the extensibility we’ve just gained.
Extending Health
Let’s imagine our game now has a new NPC type that we need to implement.
This NPC can only be killed by high damage weapons, and it always takes 5 hits to kill them.
They also become invulnerable for 5 seconds after being hit.
The bad option
We could modify our health class, add a bool field in there that we check in the editor for the NPCs that we want to use this behavior. But we don’t know how many other types of health interaction we’ll need that could cause the Health class to balloon into a mess.
And we wouldn’t be following our single responsibility principal…
What should we do? – The good option
Let’s create a couple new files and modify our existing ones.
First, we’ll want to create an interface for health, named IHealth.cs
If you haven’t used interfaces before, you can get a quick understanding of how they work here – http://unity3d.college/2016/09/04/unity-and-interfaces/
This interface says that our classes implementing it must have a TakeDamage method that has a single integer parameter. It must also have the two events we need for OnHPPctChanged and OnDied.
StandardHealth.cs
Our initial health.cs class was pretty standard for a health system. Because we’ll be adding new ones, let’s rename it from “Health” to “StandardHealth” (remember we have to rename the file as well).
The interface
We’ve also added IHealth after MonoBehavior on line 4. This tells the compiler that our StandardHealth class must implement the IHealth interface, and that it can be used for anything requiring an IHealth reference.
It’s Broken!
We haven’t even added the new health type yet, and we’ve already broken the project…
Because we renamed health, our references to the class have probably broken (unless we used the rename tooling in our editor).
Even if we didn’t break them, we still need to change our code to use the interface instead of the StandardHealth.
Let’s update NPC.cs first. We’ll replace Health (or StandardHealth) with IHealth on line 7.
We’ll do the same thing for HPBar.cs on line 11.
And repeat for NPCParticles.cs on line 9.
Let’s add that new health type finally!
Now we’ll create a new Health type called “NumerOfHitsHealth“.
Like our StandardHealth, this implements the IHealth interface, so it can be plugged in anywhere we use health on our NPC.
Unlike the standard health component though, this one completely ignores the amount of damage done, and dies after a set number of hits.
In addition to that, it adds an invulnerability timer. This prevents the NPC from taking damage more than once every 5 seconds.
Wrap Up
So now we’ve completely swapped out the health mechanics of this NPC, without needing to touch the NPC code at all (other than our initial conversion to use an interface).
If we decide to add more ways to manage health, we can simply create another implementation of IHealth, and drop that component onto the NPC.
Some other possible options might include
- NPCs that take a single hit and lose HP over time for each hit
- NPCs that regenerate HP where you need to kill them in a set amount of time
- NPCs that are unkillable and never have their HP drop
- NPCs that gain health when you shoot them (you could even swap to a component that heals them when they’re hit instead of damaging them at runtime)
- Tons of other crazy ideas I haven’t come up with in the last 60 seconds.
Using the Single responsibility principal will make your development process much smoother. It forces you to think about what you’re doing and helps discourage sloppiness. If used properly, your job will become easier, code will be cleaner, projects will be more maintainable, and you’ll be a happier person!