ScriptableObjects and Domain Reload
While disabling Unity’s domain reload feature provides quite a boost in scene editing speed, it also has its drawbacks: every object in your game now has to take into consideration that it could be re-used between different play sessions. This, in particular, is impactful if you’re using ScriptableObjects as something more than mere data bags. To handle these problems in a real-world scenario, I prepared a simple base class that can be used to avoid the major pain points.
In this post I want to provide a high-level overview of the problem and my solution to it.
What is domain reload?
Application domains are a .NET feature used to provide some level of isolation between assemblies running inside the same process. This is clearly a very useful feature in an environment like Unity, which needs to run arbitrary code without risking to break the editor in the process.
This feature is also used to provide an environment very similar to that of the actual game: by unloading and recreating an application domain, Unity can simulate a new startup of the game at every play operation, starting from a blank slate.
This has quite the downside though: recreating a domain is an expensive operation. This brings to the dreaded, long startup time when pressing “play” on a big project: all the code of the game must be completely removed from memory, then reloaded, including that of every package you’re using, which can be quite a lot.
Note also that splitting your code in multiple assembly definitions does not help in this case: while it can reduce compilation time (only the modified assemblies must be recompiled), a domain reload forces all the code to be reloaded, indipendently of the assembly it belongs to.
So, no domain reload?
Because of this problem, Unity introduced the option to disable the domain reload, together with the option to also not reload the scene. This solution does not mean that domains are never reloaded: whenever the code is changed, either yours or that of an imported package, that code must be recompiled, and thus the whole domain must be rebuilt. But, at least, this option avoid a full domain reload every time “play” is pressed.

This brings a new host of problems. Since the domain is not reloaded, a certain amount of data in your objects will not be reset automatically to its default value, and some lifecycle methods will not be called as usual. Exact details about the implications are documented by Unity, and it’s good to read about them, otherwise one could have very weird unexpected behaviors, where the same code works the first time it’s tested after changing the code, but not the second time!
One type of objects that can cause major problems are ScriptableObjects.
ScriptableObjects in no Domain Reload scenarios
A ScriptableObject is a serializable object often use to store data in a way that is easily accessible by other ScriptableObjects and MonoBehaviours, independently of their scene or position in the scene. A typical scenario is that of global configurations.
Using them as a simple data bag is completely fine, but since they are objects, they can also be used to provide methods, and compute data at runtime. Which leads to the problem: what happens to its static and instance fields when we are in a no-domain-reload scenario?
This obviously means that the ScriptableObject has the duty to clean up after itself when moving from editor to play mode. Not only that, but at runtime this can also happens when the scriptable object is enable or disabled, which happens in very specific scenarios, but… these are actually linked to domain reloads! This means that until a domain reload needs to happen, the ScriptableObject won’t receive an OnDisable, and neither will get an OnEnable. These methods are the ones typically used when symmetric setup and cleanup are performed, but in this case they can’t be the be-all and end-all solution.
Luckily, Unity offers some hooks available in edit mode to know when we’re switching between edit and play mode, and we can take advantage of that to perform cleanup and setup operations. In a way, we can think of removing the domain reload system as moving the responsibility of these operations from Unity (through expensive domain reload operations) to your code (through lightweight cleanup and setup methods).
The ScriptableObjectSetupSupport base class
This brings me to ScriptableObjectSetupSupport:
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
/// <summary>
/// A base class for scriptable objects that need some form of setup and cleanup for internal data structures. This
/// class supports both objects running in play mode and edit mode, both with domain reloading active or not.
/// </summary>
public abstract class ScriptableObjectSetupSupport : ScriptableObject
{
protected virtual void OnEnable()
{
#if UNITY_EDITOR
EditorApplication.playModeStateChanged += StateChanged;
#endif
GlobalSetup();
}
protected virtual void OnDisable()
{
#if UNITY_EDITOR
EditorApplication.playModeStateChanged -= StateChanged;
#endif
GlobalCleanup();
}
private void StateChanged(PlayModeStateChange playModeStateChange)
{
if (playModeStateChange != PlayModeStateChange.ExitingEditMode) return;
GlobalCleanup();
GlobalSetup();
}
/// <summary>
/// Method invoked every time there's the need to set up the scriptable object. This method should take care of
/// creating objects and/or initializing them.
/// </summary>
protected abstract void GlobalSetup();
/// <summary>
/// Method invoked every time there's the need to clean up the scriptable object. This method should take care
/// of disposing objects and/or de-initializing them.
/// </summary>
protected abstract void GlobalCleanup();
}
This class is based on two cases:
OnEnableandOnDisable: they still perform a setup and cleanup operation, which will be linked to domain reload, but also register to play mode status changes. These methods will be used both in the domain-reload and no-domain-reload cases.StateChanged: this only listen to when we’re existing edit mode; this is our last chance to actually set up the objects, because waiting for theEnteringPlayModestage could be too late in some scenarios to perform setup. This will mostly cover the no-domain-reload scenario.
Using this instead of ScriptableObject as the base class for your own ScriptableObjects will ask you to implement the two methods GlobalSetup and GlobalCleanup, whose purpose is pretty clear. Once implemented, you can have the assurance that cleanup and setup will be performed at the right time both in domain reload and no-domain-reload situations, both in edit and play mode.
This solution is based on this discussion, which provides a non-reusable implementation.