A Brief Treatise on Events and the Object Lifecycle in Unity

The Problem

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 experienced by 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.

  1. The object lifecycle in Unity is leaky.
  2. 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?

private List targetList = new List();
public void OnTriggerEnter(Collider target) {
Target t = target.GetComponent();
if(t != null) // In case we collide with something that isn't a target.
targetList.Add(t);
}
public void OnTriggerExit(Collider target) {
Target t = target.GetComponent();
if(t != null) // In case we collide with something that isn't a target.
targetList.Remove(t);
}
public Target SelectTarget() {
for(int i = 0; i < targetList.Count; i++) {
if(targetList[i] == null) {
targetList.RemoveAt(i);
i--;
continue;
}
// Whatever logic for checking target validity/desirability goes here:
if(TargetIsGood(targetList[i]))
return targetList[i];
}
}

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:

if((targetList[i] == null) || (!targetList[i].enabled)) {

Oops, that’s still wrong. What if the object isn’t active?

if((targetList[i] == null) || (!targetList[i].enabled) || (!targetList[i].gameObject.active)) {

Dangit. We just added another very minor drain on performance needlessly with that .gameObject call.

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.

if((targetList[i] == null) || (targetList[i].gameObject == null) || (!targetList[i].enabled) || (!targetList[i].gameObject.active)) {

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 SelectTarget() method would look like if OnTriggerExit was called when one of the participants was Destroy()’d, disabled, or deactivated:

public Target SelectTarget() {
foreach(Target t in targetList) {
// Whatever logic for checking target validity/desirability goes here:
if(TargetIsGood(t))
return t;
}
}

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.

Proposed Solutions

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.

Singletons:

void OnLevelWillUnload(string newLevelName, int newLevelIndex);

Called when Application.LoadLevel is called, Stop is pressed, or the application quits. After OnDisable, and OnDestroy. Before the new level is loaded. Used for cleaning up singletons gracefully.

void OnLevelIsLoading(string newLevelname, int newLevelIndex, bool isAdditive);

Called when Application.LoadLevel is called or Play is pressed. After level is actually loaded, but before OnEnable, Awake, Start, or OnLevelWasLoaded handlers are called. Used for setting up dynamic elements of a scene gracefully, as if they were a static part of the scene.

void OnDestroy();

Differentiated from 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 Destroy()’d, when a scene change happens, or when the application is quit / Stop is pressed.

Edit-time workflow:

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 OnEnabled/OnDisabled 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 feature:

void OnSerialize();
void OnDeserialize();
static void OnSerialize();
static void OnDeserialize();

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:

void OnPlayerPref();

Executed before Awake, and whenever a Set method is called on PlayerPrefs. One possible alternative would be something attribute based. For example, imagine:

[Preference("UseProjectileLights")]
public bool useProjectileLights {
get { return enabled; }
set { enabled = value; }
}

Or:

private int _lod;
public Mesh[] lods;
public MeshFilter filter;
[Preference("MeshLOD")]
public int LOD {
get { return _lod; }
set {
_lod = value;
filter.sharedMesh = lods[_lod];
}
}

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)”.

Conclusion

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.

unity 1779 words, est. time: 355 seconds.

« Pathfinding in Unity Unity's Reflective/Diffuse is Broken. Here's the Fix. »

Comments

Copyright © 2016 - Jon Frisby - Powered by Octopress