People who are past their first steps with Unity will, at some point, feel ready to make a build of their project; be it to send it to others or just see the fruits of their work as a standalone thing that actually works outside the work environment it was created in. I’ve often seen people disappointed because their build unexpectedly runs differently than the project tested in the editor’s play mode.
This article highlights some of the causes for divergent build behavior.
Undefined MonoBehaviour event order
The cause that I consider the easiest to run into is code relying on specific MonoBehaviour
event order. For example, code in Start – consider these two components:
public class ThingOwner : MonoBehavior { private Thing thing; private void Start() { thing = GetComponent<Thing>(); } public void DoStuff() { thing.DoThings(); } }
public ThingOwner thingOwner; // Set in the inspector private void Start() { thingOwner.DoStuff(); }
The second component uses the first one (ThingOwner
) to run DoStuff
in Start
. It might be that in the editor, this consistently worked because the ThingOwner
‘s Start event ran before the second component’s. However, you have no guarantee for this to always work because by default, the order in which Unity runs MonoBehaviour
events in your components falls under the undefined behavior category. It might consistently work the same way in some contexts, but you can never rely on that.
This is why you need to design your project to enforce consistent behavior, which sadly is a thing you just have gather some experience with. The semi-consistency in which these events run in the editor doesn’t exactly help you identify any problems. You have to know them yourself.
One way to take control over the event order is the Script Execution Order. However, I wouldn’t recommend using this to design your project, as it makes your script’s ability to properly function dependent on something that’s not even part of your codebase.
Instead, familiarize yourself with the events that Unity sends to your MonoBehaviour
s and how they relate to each other.
In this case, the solution is to not use Start
for value initialization, like caching a component reference acquired through GetComponent
. Even though official and unofficial learning resources have advocated this through the years, it’s not a good practice. Instead, use Awake
.
private Thing thing; private void Awake() { thing = GetComponent<Thing>(); }
Just like Start
, Awake
is called only once on each component at the start of its active lifetime. However, it is designed to be used for field initialization, and it works very nicely for that in more than one way.
- At scene start, all
Awake
calls happen before allStart
calls. An object performing initial logic inStart
can rely on another object having been initialized inAwake
, given that it is active. Awake
is called right beforeOnEnable
on a component. Code inOnEnable
can rely on “its own” object having been initialized in Awake.Awake
andOnEnable
are both called within an Instantiate call. If you work with a freshly instantiated object, you can rely on the object having been initialized in Awake, given that is active.
To illustrate the last point, consider that this code is guaranteed to work just fine:
var thingOwner = Instantiate(thingOwnerPrefab); thingOwner.DoStuff();
Of course, the thingOwnerPrefab
needs to actually have a Thing
component, and the ThingOwner
component needs to be implemented using Awake
, as shown above. The instantiated object’s Start
event will, in fact, be executed at the end of this frame, and with that, after the DoStuff
call.
The initialization part of any component’s lifecycle is something that can be done rather sloppily or very cleanly. Going for a clean implementation not only allows you to write quality code around it (like using a properly initialized object right after instantiating it), but also helps your code to behave consistently, no matter the context.
You can use scripts like this to experiment and learn about the order in which MonoBehaviour
events happen. And be aware that Awake
and Start
are just an example, and that inconsistencies can happen in many other places, too – like Update or collision events.
Platform dependent code
This one’s rather obvious as you usually don’t have this sort of code in your project unless you know what you’re doing. But for short: With preprocessor directives, you can have the compiler use different code depending on the context, like compiling for another platform or considering whether or not we’re making a debug build.
var i = 10; #if UNITY_EDITOR i = 20; #endif Debug.Log(i);
Even though it’s not very likely that you wrote code that works in the editor but doesn’t in your build, it’s not impossible. So if your build works in unexpected ways, consider double-checking your preprocessor directives.
Exceptions in Builds
Another unlikely, but not impossible issue is exception catching. Unity throws different Exceptions in the same situation while in the Editor vs. while in a build. Consider this code:
using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(Text))] public class MissingReferenceExceptionTest : MonoBehaviour { public Light light; private void Start() { try { light.color = Color.white; } catch (System.Exception e) { GetComponent<Text>().text = e.GetType().ToString(); } } }
If you don’t assign a Light
component to the public field, the line assigning a color will throw an exception. The code writes the exception name into a UI Text component, so we can see it in the editor and in the build.
In the build, we get a NullReferenceException
, as one might expect. However, in the editor, we instead get an UnassignedReferenceException
.
And it gets even worse: When we use the return value of a GetComponent
call that doesn’t find the component, we get a MissingComponentException
upon accessing it, instead of the NullReferenceException
we’d get in a build.
Without having an official statement on this, I very much assume that Unity does this solely in order to produce some additional information for you.
However, this means that if you use explicit catch parameters, you run into inconsistent behavior:
try { // something } catch (MissingComponentException) { // React to missing component }
The code above might work in the editor, but since NullReferenceExceptions
are not caught, they’d just propagate through your code in a build.
Most people use null checks with if statements anyway, so this case isn’t very likely to happen. But nonetheless, it is a thing that is, by design, working differently in a build than in the editor.
Conclusion
While preprocessor directive issues and component catching are both rather unlikely to happen, relying on the unreliable MonoBehaviour
event order is something that I’ve seen very often. Check your code for places where you rely on undefined behavior to be consistent, and make that code expect the unexpected.