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!
When you recompile while the game is playing in the editor, the following series of events happens:
- Active scripts are disabled.
- All the live objects are serialised (invoking custom callbacks where applicable).
- The assembly is unloaded.
- The changed code is compiled.
- The new assembly is loaded.
- The previously saved state is reloaded (objects are instantiated as needed, fields populated, custom callbacks executed).
- All previously active scripts and components are enabled.
And this is where your console is usually filled with hundreds of null pointer exceptions...
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.
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 fromMonoBehaviour
,ScriptableObject
,Component
andEditorWindow
, for instance) - certain primitive types (
bool
,int
,float
,string
etc.) but not necessarily all of them (e.g.uint
andlong
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
List
s 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
andFunc
) - 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.
C# has a clear distinction between value and reference types: struct
s and primitive types have value semantics and are stored inline, while class
es 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- infamously, if a type marked as
Serializable
is recursive, it would normally induce infinite inlining, so Unity has an internal limit of 7 nesting levels when storing such data (in my opinion it should completely disallow such usage)
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.
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.
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.
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;
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.
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.
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.
Hello Gergely,
thank you so, so much for writing up your article! And also for writing it in such a short time. I can't imagine how much effort you put into trial-and-error and experimentation to collect all these information. Why is there no official documentation regarding hotswapping anyway? I hope you advertise your article a lot.
Following are some thoughts that came to mind when first reading your article:
If you have some useful links to more information please provide them.
Is there an official documentation other than the one about "script serialization"?
I know auto-properties:
public int foo { get; set; }
But what are "backing fields"?
What does invisible exactly mean? It makes sense not to serialize const and readonly but do they keep their original values or are the set to their default values too?
What about static constructors?
Do you have a canonical code snippet on how to implement manual reference handling? I imagine a naive implemention is not to difficult but it probably has some not so obvious edge cases that someone else already thought of (circular references, null values etc.).
Interesting, so I can avoid writing serializers for Dictionaries if I just define concrete types for every combination, like so:
Thanks again! I will try to adopt our code base accordingly.
Cheers, Gerold.