Almost every headache I’ve had with Unity has ultimately come down to two simple but fairly amorphous problems. My goal is to make the nature and implications of those problems clear, and present a possible solution that Unity Technologies could implement to (hopefully) solve them.
EDIT: Matthew Wegner suggested to me an alternative to the events here that
are most useful for more graceful singletons. Specifically, the notion of a
MonoManager with a handful of events that would wrap “around” the lifecycle
MonoBehaviours. I will write a post on this in the not too
distant future, but it’s a far better idea than the comparable extensions to
MonoBehaviour I proposed below. Note that it doesn’t supplant all of what
I proposed, but the unified result is much simpler than my initial proposal.
- The object lifecycle in Unity is leaky.
- The event model in Unity is not comprehensive enough to compensate for this.
By “leaky”, I am referring to what Joel Spolsky meant when he coined the term “leaky abstraction”. In short: You need to know too much about how the abstraction is implemented to use it effectively. (Note that I very specifically do not mean “leaky” in the sense of memory management.)
For anything but the most trivial use-cases, the Unity object lifecycle becomes
leaky. An obvious and long-standing example is the scenario of using triggers
to determine when an object comes into range of a turret. One is likely going
to code the turret to keep a list of all objects currently within range, and
then go down that list to find a suitable target to fire at. Once an enemy is
destroyed, one might intuitively want it to cease to exist – to be
Destroy()‘d. BUT, if you do that, you quickly encounter a nasty problem:
OnTriggerExit doesn’t get called. Suddenly your turret’s target list may not
be quite valid.
Ok, ok. Just do null checks when traversing the list, right?
Ugh. What’s wrong with this code? Let me count the ways…
You just made determining whether or not a turret has any targets into an O(n) operation instead of O(1).
That code is considerably less clean and readable than it would be without having to have what out to be needless null checks.
One more shallow cut for performance (branch prediction, amount of code that can live in the code cache, etc). It’s easy to wind up with a nasty performance problem due to lots of “diffuse” issues like that. Hard to find in profiling, and hard to fix since individually none of them ever really matter. But they matter when you have lots of turrets, and/or lots of similarly nasty code elsewhere.
It’s wrong. It doesn’t take into account when the object becomes disabled. Ok, so change the if statement:
Oops, that’s still wrong. What if the object isn’t active?
Dangit. We just added another very minor drain on performance needlessly with
And to top it all off, it’s still wrong. Turns out, keeping a reference to a
MonoBehaviour across frames is bad because it doesn’t go away, even if the
GameObject to which it is attached does.
So essentially, using triggers/colliders in a stateful way means knowing all
the different features of the Unity object lifecycle that are relevant and
writing code to check them. Imagine, for a second, what our
method would look like if
OnTriggerExit was called when one of the
Destroy()’d, disabled, or deactivated:
This is clearly much better code to be writing, no? Unfortunately, this is not the ONLY place this comes up. There are a number of common patterns that arise that become very verbose and/or error-prone when using Unity. Patterns such as singletons, for example. Don’t manage your singletons right, and you get nastiness when changing scenes. Manage them 'right’ and you have headaches binding objects together. Preferences that affect the game (think LOD-related preferences that can be changed mid-game) are another painful area. Internationalization? Also painful.
In order to make the event lifecycle smoother to work with, I’ve come up with some possible changes to the Unity event model that I think would do the trick – or at least help get Unity much closer.
I’ll present them in the context of the problem they are most immediately applicable to, but shoring up the event model and making the lifecycle less leaky should prove enormously helpful for many more use cases than these.
Application.LoadLevel is called, Stop is pressed, or the
application quits. After
OnDestroy. Before the new level is
loaded. Used for cleaning up singletons gracefully.
Application.LoadLevel is called or Play is pressed. After level
is actually loaded, but before
OnLevelWasLoaded handlers are called. Used for setting up dynamic elements of
a scene gracefully, as if they were a static part of the scene.
OnDisable because sometimes one doesn’t want to do
“cleanup” work simply when something is disabled but rather only when it is
truly going away. The object of course can ‘go away’ when it is
when a scene change happens, or when the application is quit / Stop is pressed.
Unity will already do dynamic code reloading without stopping your game. This
is an awesome feature, especially when working with UnityGUI. Unfortunately
it’s nearly impossible to avoid writing code that breaks it. You wind up having
code reloading for the first week or two of a project, then you lose it and
never get it back. Ouch. Some of this is due to Unity supporting a relatively
limited set of types for serialization, but much of it comes from the lack of
any form of handling for statics. Right now, you get
called when objects are deserialized/serialized which almost does the trick,
but if you use that for preserving complex state, or managing static variables,
you lose the ability to use the enabled state to simply determine whether an
object is enabled (or at the least you add a lot of overhead to making use of
enabled). Having a few event handlers to differentiate
serialization/deserialization from an object being enabled would make it
possible to write relatively clean code to integrate gracefully with this
Editor only, of course to facilitate code reloading. Calling order would be static version, then all instance versions.
An interesting bonus would be if these event handlers could be stripped from
builds, so that the relevant code didn’t need to ship with the game. This would
be especially good for iPhone/Wii and whatever future limited form-factor
platforms are to be supported but would provide a small benefit even to web
players. (He said, after stripping 640KB of dead code from Hordes of Orcs…) I
had initially considered suggesting they be events on
Editor, but it occurs
to me that per-instance handling is more complex, and it would force you to
code the actual
MonoBehaviour with less privacy than is otherwise needed for
member data which is usually a bad idea.
Preferences and runtime configuration of the game:
This one is a bit more complex in terms of possible solutions, as there are some subtleties involved and one wants to ensure that making the lifecycle be not-leaky doesn’t impede performance unduly for common operations.
One possibility is an event handler:
Awake, and whenever a
Set method is called on
PlayerPrefs. One possible alternative would be something attribute based. For
I personally prefer this latter approach as it codifies more information about dependencies in an easily machine-processed manner. (Think dependency analysis tools.)
Logical States vs. Physical State Transitions
Much of the chaos is caused by events only happening when that event actually happens, which seems perfectly reasonable but in order for the abstraction to hold the events really need to line up with the intuitive, logical model of states that is implemented by Unity.
For example, consider the state of collision. An object is colliding, or it is not. If its collider overlaps another collider, it’s colliding right? Well, not quite. You only get collision messages if both objects are enabled and active as well, and of course, the objects have to still exist. Implicitly, an object is in a state of collision only if all of these conditions are met. So it makes much more sense intuitively to get messages about transitioning into and out of this logical state whenever any of the conditions change rather than simply the single condition of “colliders overlap/cease to overlap (while all other conditions are true)”.
There’s been a good effort on the wiki to document the flow of events in Unity and how they tie in to the object lifecycle but really this needs to be modeled from the standpoint of the implicit states to really become clear. Maybe I’ll find the time to graph this out at some point.
As a good friend stated to me: “Object lifecycle is a damned tricky problem”, and on top of that Unity is a relatively young product that has taken a fairly radical approach (to very impressive ends) so it’s not surprising that this would be an area needing some work. With a little more effort and the right framing of the problem, the Unity Lifecycle could be quite pleasant to use for even fairly complex scenarios.