Skip to content
Menu
13Pixels Blog 13Pixels Blog
  • Hi, it’s FlaSh.G!
13Pixels Blog 13Pixels Blog

Things that can make a difference in Unity’s play mode compared to your build

Posted on 2019-04-282019-06-05 by FlaSh.G

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 MonoBehaviours 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 all Start calls. An object performing initial logic in Start can rely on another object having been initialized in Awake, given that it is active.
  • Awake is called right before OnEnable on a component. Code in OnEnable can rely on “its own” object having been initialized in Awake.
  • Awake and OnEnable 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.

Unity gives you a lot of additional, Unity-specific information and hints in some error cases.

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.

Recent Posts

  • Unity and the mysterious singleton
  • Communicating Objects
  • Reference Semantics for Beginners
  • How Unity destroys objects
  • Unity Game Initialization

13Pixels in the Unity Asset Store

Soda - ScriptableObject Dependency Architecture

Check out Soda and more assets in the Unity Asset Store!

Discuss with other Devs

Join the 13Pixels Slack Workspace for discussions and updates.

©2025 13Pixels Blog | Powered by WordPress & Superb Themes