MonoBehaviour is evil and everyone should know it
Here, I said it. It mixes serialization, magic methods, callbacks to pretty much everything and undefined access level of all that in a single, messy soup. MonoBehaviour
with two unimplemented methods (which is something Unity itself recommends against), two unnecessary using
directives and no namespace is the first thing an aspiring dev sees when he starts to code in Unity. It’s just sloppy.
This is the post where I try to figure out MonoBehaviour
in a sane way. I’m not going to be overly concerned about performance here, though there will be some remarks regarding Update
method and its friends. It’s mostly about how to make it look and behave according to the least astonishment principle. It’s about making it a sane piece of code.
When I started writing, I realised that some of the things mentioned would deserve an article of their own. So this may get edited in the future.
So, let’s start by enumerating the stuff MonoBehaviour
actually does:
- Allows serialization from the Editor. We can serialize simple types, strings, things derived from
Unity.Object
and those marked asSystem.Serializable
. - Gives access to Unity magic methods:
Update, Start, Awake, OnEnable, OnDisable
and rest of the gang. I say "magic" methods, since they are not inherited: they're just detected in code "magically" (via reflection, that is) and called by the engine when appropriate. - Allows creating own components of sort - a script derived from
MonoBehaviour
can be attached to aUnityEngine.GameObject
and thus exist on a scene. This idea of being a special type of component goes further:MonoBehaviour
cannot really be brought to life with a constructor call - in order to create one, we need to callGameObject.AddComponent
, passing required type. Unity will create an object for us. And while we're at it, we can also find it later usingGameObject.GetComponent
. - Can be found using
FindObjectOfType
if it's present on a scene - Allows starting a
Coroutine
, poor man's UnityTask
.
That's a lot of stuff, isn't it? To be completely honest, that’s expected and it’s hard to blame Unity Technologies for it; after all, it’s a scripting bridge between the engine and game specific code. That being said, the class is anything but a single responsibility, allows flexibility in places where it isn’t really needed and actually encourages bad practices. It also has a number of design problems. So, let’s write them down!
Inconsistent serialization code
The easiest way to make something serializable is to make it a public
field. The problem with that? Well, you now have public fields in your code, even if logically they don’t need to be public. As to why it’s a problem, I’ll just leave this. Fix? There’s a SerializeField
attribute - use it!
Half-initialized objects
As mentioned above - you cannot use a constructor when making a new component - which makes object initialization an issue. Now, you can (by default*) either
a) use serialization and assign everything in Inspector,
b) call GetComponent
, FindObjectOfType
and assemble everything locally,
c) create required dependencies locally or
d) design an object that doesn’t need dependencies at all…
So, assuming that option D is out of the question, we are left with 3 subpar solutions. Inspector would work alright in a small project, but once it grows in size, we enter a realm of MissingReferenceException
hell. We are also limited to the stuff that can be serialized (duh!). GetComponent
guarantees that we will at least try to get that reference, but the component could be missing on the scene entirely and we’ll never know until the game is running. Making dependencies locally could work for simple cases - but this breaks the moment two behaviours need to share a single resource.
Solution? Unless we want to solve it with specialised frameworks* I suggest mixing the approaches. For everything except dependencies - use serialization. If there’s a lot of stuff to be passed in, you may write a simple serializable class that will hold parameters together; if many things need to share those parameters, you can always make a ScriptableObject
for that. There is also Header
, but I wouldn’t use it**.
Now, for dependencies - this is where it becomes really tricky. The best you could do would be to keep the dependencies to the local object - because then you can decorate the class with attribute RequiresComponent
and safely call GetComponent
. Dependencies outside the scope? My suggestion would be to use serialization and then, in Awake
, assert that they are not equal to null. You can also use FindObjectOfType
as a last resort - but at this point, you should probably look at the stuff with an asterisk*.
* Obviously, there are dependency injection frameworks for Unity - but that’s a topic for another post.
** Personally I don’t like Header
attribute, for the same reason that I avoid using #region
in my code: if you need to use them, it means you are trying to shove too many things in one place. Just split the class into more!
Script execution order bonanza
Now, this piece is I guess the most opinionated one, so proceed at your own risk - but Script Execution Order sucks. I mean, I totally get why it exists - but it's a hack, trying to cover an underlying problem: there's no control over which objects actually need to be included in the game loop calculations. The most you can do is either to not implement Update
at all - which is hardly a solution - or disable a whole MonoBehaviour
, which will have a side effect of disabling every other Unity callback method as well. Not cool.
Now, is there any solution to that - except from manually returning from Update
at the very beginning? Well, yes - and it will have a nice side effect of being performance friendly. We start out by writing a single update method - let's call it an UpdateManager
. Then, give it a list of all objects that need to be updated, optimally behind a simple interface like IUpdatable
or something. Need to stop updating a single object? Just remove it from the list, done. Want to have a game loop code in a class that isn't MonoBehaviour
? No problem, just implement the interface and pass it. You can even manually manage how often certain types should be updated. Pure profit, right? Sure, but with an important gotcha: single unhandled exception will stop every piece of logic that would run in a loop after the exception.
So, apart from that, MonoBehaviour
is golden, right? Well, no. But these are the worst. From my experience, once it becomes a simple "pass me some params from the outside and I will handle collisions", it gets waaay more manageable. Just, you know, don't overuse it, right? MonoBehaviour
is evil, but it was created this way, and everyone should know that.