Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Unity hotswapping notes

Unity hotswapping notes

Unity has built-in support for hotswapping, which is a huge productivity booster. This feature works not only with graphics assets like bitmaps and meshes, but also with code: if you edit the source and save it, the editor will save the state of the running game, compile and load the new code, then load the saved state and continue where it left off. Unfortunately, this feature is very easy to break, and most available 3rd party plugins have little regard for it.

It looks like there’s a lot of confusion about hotswapping in Unity, and many developers are not even aware of its existence – which is no wonder if their only experience is seeing lots of errors on the console when they forget to stop the game before recompiling... This document is an attempt to clear up some of this confusion.

Nota bene, I’m not a Unity developer, so everything below is based on blog posts and experimentation. Corrections are most welcome!

The basic flow of hotswapping

When you recompile while the game is playing in the editor, the following series of events happens:

  1. Active scripts are disabled.
  2. All the live objects are serialised (invoking custom callbacks where applicable).
  3. The assembly is unloaded.
  4. The changed code is compiled.
  5. The new assembly is loaded.
  6. The previously saved state is reloaded (objects are instantiated as needed, fields populated, custom callbacks executed).
  7. All previously active scripts and components are enabled.

And this is where your console is usually filled with hundreds of null pointer exceptions...

Hotswapping vs. persistence

It’s important to understand that hotswapping and persistence (storing scene, prefabs and scriptable object assets) use slightly different flavours of the serialisation system. The biggest difference is that during hotswapping private fields and auto-properties (i.e. those with a backing field) are also saved and restored when the new code is loaded, while these fields are not persisted in assets by default.

What exactly is preserved during hotswapping?

While remembering the rules is useful, there’s a simple trick one can use: turn on the debug mode of the inspector view. In this mode, you can see the raw fields, including private ones and property backing fields in read-only mode. If you don’t see a field here, it means that it has a type not supported by the serialisation system or it’s explicitly excluded (annotated with the NonSerialized attribute).

According to the documentation, the following types are supported at the moment:

  • classes inheriting from UnityEngine.Object (this includes everything that inherits from MonoBehaviour, ScriptableObject, Component and EditorWindow, for instance)
  • certain primitive types (bool, int, float, string etc.) but not necessarily all of them (e.g. uint and long only got support recently, so double check before use)
  • enums
  • classes and structs annotated with the Serializable attribute (however, these are treated with value semantics, see below)
  • arrays and Lists of all the above (but not when nested, e.g. List<List<int>> won’t work!)

Also, fields cannot be static, const, or readonly, otherwise they will be invisible to the serialisation system.

Here’s a non exhaustive list of what’s not supported:

  • interface types
  • generic types
  • standard library types
  • delegates of any kind (including Action and Func)
  • inheritance for types that aren’t derived from UnityEngine.Object
  • coroutines (they are stopped during hotswapping and never reinstated)

Some of these limitations can be worked around, as we’ll see.

Value vs. reference types

C# has a clear distinction between value and reference types: structs and primitive types have value semantics and are stored inline, while classes are reference types that live on the heap. The serialisation system has such a distinction too, but the line is drawn elsewhere. If a type is a descendant of UnityEngine.Object, it is serialised as a reference. In every other case (value type or custom class marked as Serializable) the type has value semantics in the context of serialisation. This has some consequences:

  • the class is serialised inline: even if there’s one physical object, its data is going to be copied for each reference to it
  • references are lost: every reference to an object becomes a reference to a separate object (with identical contents) after hotswapping
  • null cannot be represented: it is replaced by an instance whose fields are initialised with default values

One notable gotcha to remember: null strings turn into empty strings during hotswapping. Generally speaking, you should never check values of these types for null if you want to support hotswapping. Methods like string.IsNullOrEmpty come in handy in many situations.

Customising the (de)serialisation step

Unity introduced the ISerializationCallbackReceiver interface, which provides a hook into the serialisation process. The purpose of this hook is to convert unsupported types into supported ones and back, thereby increasing the expressiveness of the system. For instance, Dictionary is not supported by default, but it can be converted into a list of keys and values (which the system already knows how to deal with), then it can be reconstructed from those lists after reloading.

Read the documentation carefully, because this interface comes with a lot of warnings. It’s especially important to keep in mind that it might be executed on a different thread, so even innocent looking things like checking Application.isPlaying might lead to an error. In the end, however, it’s a powerful tool that lets us work around some of the above mentioned limitations.

Supporting static fields

The easiest way to deal with static fields is to wrap them in properties with logic to retrieve or reconstruct the instances on demand:

private static Stuff _instance;

public static Stuff Instance
{
    get
    {
        if (_instance == null)
        {
            _instance = ...; // new / FindObjectOfType / LoadResource etc. as applicable
        }
        return _instance;
    }
}

For instance, if this object lives on the scene, it will be reconstructed by the serialisation system, then the reference will be retrieved on demand the first time it is referenced through the getter.

Should you find the need to save something static out of band or perform some other operation before hotswapping, one rarely mentioned weapon is the AppDomain.CurrentDomain.DomainUnload event. Once I needed to use it to be able to cleanly hotswap a 3rd party library, but it might be useful for other purposes as well. Use your imagination.

Supporting generic types

The serialisation system simply ignores generic types other than List<T>. However, there’s a simple trick to make them visible: convert them into a concrete type with inheritance. For example, if you created some pooled list type called PooledList<T> while optimising your memory usage, you can use it in the following manner:

[Serializable] public class PooledGameObjects : PooledList<GameObject> { }

public PooledGameObjects Pool;

Of course, this assumes that PooledList supports hotswapping, i.e. either it uses supported data structures and types internally, or it implements the serialisation callback interface to massage its data into the right shape.

If the class doesn’t have a default constructor, you need to include it in the definition, e.g. if you have to pass a capacity to the list:

[Serializable] public class PooledGameObjects : PooledList<GameObject>
{
    public PooledGameObjects(int capacity) : base(capacity) { }
}

The alternative to introducing a custom type for each usage is to add extra logic through the serialisation callback. In my opinion, the above trick is a lot more lightweight (just one line), and the new type allows the code to be a bit more self-documenting. One thing to keep in mind is that marking the original generic class as Serializable is useless, because this attribute is not inherited.

You can also create hotswap-friendly wrappers around otherwise unsupported generic data structures like Dictionary and use this trick to avoid writing a callback for every class that needs to use it.

Finally, this trick also works with the otherwise unsupported case of nested lists, just define a type for the inner list:

[Serializable] public class MaterialList : List<Material> { }

public List<MaterialList> MaterialLists;

Dealing with UI callbacks

There are two ways to subscribe to UI events: manage listeners in code or wire up methods in the scene. The latter option automatically supports hotswapping, because it explicitly stores the reference of the target object and the name of the method. However, I tend to avoid this feature, because it’s hard to manage these references and they silently break as soon as I rename the method in question. Sadly, listeners registered in code cannot be persisted, since they are delegate types.

One solution in this case is to register listeners in OnEnable and remove them in OnDisable, both of which are called during hotswap. If this is not applicable to your use case (say, you want your listener to be persistent, even when the object is not active), then you can use a custom serialisation callback to set a flag that tells your program to reconstruct the listeners. I try to use this as a last resort simply because OnEnable and OnDisable are readily available and guaranteed to run in the main thread where I can access engine functionality immediately. Note that Awake and Start are not called after hotswapping.

Coroutines

Hotswapping causes all coroutines to stop, and their state is not saved. There’s really no easy solution here; just keep this limitation in mind. Usually you don’t need hotswapping to cover all areas of your application, just enough to be able to iterate on certain elements without having to restart everything, so this is not necessarily a problem unless you rely heavily on coroutines in your gameplay logic.

Supporting interfaces and readonly fields

My recommendation is to avoid these features for fields that you want to preserve during hotswapping. If you still want to use them, then the serialisation callback is the only sensible way to do so, unless you know they are repopulated through a different mechanism (e.g. constant initialiser or constructor). This is a clear trade-off: you can use more features, but you pay by having to write more code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.