Serialization of “things” is at the very core of Unity. Many of our features build ontop of the serialization system:
The Inspector window doesn’t communicate with the C# API to know the values of a property that it is inspecting: It asks the GameObject to serialize itself, and then displays this serialized data.* Inspector window. The inspector window doesn’t talk to the C# API to figure out what the values of the properties of whatever it is inspecting is. It asks the GameObject to serialize itself, and then displays the serialized data.The Inspector window doesn’t communicate with the C# API to know the values of a property that it is inspecting: It asks the GameObject to serialize itself, and then displays this serialized data.
Prefabs. Internally, a prefab is the serialized data stream of one (or more) GameObjects and components. A prefab instance is a list of modifications that should be made on the serialized data for this instance. The concept prefab actually only exists at editor time. The prefab modifications get backed into a normal serialization stream when Unity makes a build, and when that gets instantiated, the instantiated GameObjects have no idea they were a prefab when they lived in the editor.
Instantiation. When you call Instantiate()
on either a prefab, or a GameObject that lives in the Scene, or on anything else for that matter (everything that derives from UnityEngine.Object
can be serialized), we serialize the GameObject. It then creates a new GameObject and we “deserialize” the data onto the new GameObject. Next, run the same serialization code again in a different variant, where we use it to report which other UnityEngine.Objects
are being referenced. We check for all referenced UnityEngine.Objects
if they are part of the data being Instantiated()
. If the reference is pointing to something “external” (like a texture) we keep that reference as it is. If it is pointing to something “internal” (like a child GameObject), we patch the reference to the corresponding copy).####Instantiation * Instantiation. When you call Instantiate()
on either a prefab, or a GameObject that lives in the Scene, or on anything else for that matter (everything that derives from UnityEngine.Object
can be serialized), we serialize the GameObject. It then creates a new GameObject and we “deserialize” the data onto the new GameObject. Next, run the same serialization code again in a different variant, where we use it to report which other UnityEngine.Objects
are being referenced. We check for all referenced UnityEngine.Objects
if they are part of the data being Instantiated()
. If the reference is pointing to something “external” (like a texture) we keep that reference as it is. If it is pointing to something “internal” (like a child GameObject), we patch the reference to the corresponding copy).####Instantiation
Saving. If you open a .unity
Scene file with a text editor, and have set Unity to “force text serialization”, we run the serializer with a yaml backend.
Loading. Might not seem surprising, but backwards compatible loading is a system that is built on top of serialization as well. In-editor yaml loading uses the serialization system, but also the runtime loading of Scenes, Assets and AssetBundles that uses the serialization system.
Hot reloading of editor code. When you change an editor script, we serialize all editor windows (they derive from UnityEngine.Object
!). Then we destroy all the windows, unload the old C# code, load the new C# code, recreate the windows, and then we deserialize the datastreams of the windows back onto the new windows.
Resource.GarbageCollectSharedAssets()
. This is our native garbage collector. It’s a different thing than the C# garbage collector. It is the thing that we run after you load a Scene, to figure out which things from the previous Scene are no longer referenced, so we can unload them. The native garbage collector runs the serializer in a variation where we use it to have GameObjects report all references to external UnityEngine.Objects
. This is what makes textures that were used by Scene1, get unloaded when you load Scene2.
The serialization system is written in C++. We use it for all our internal GameObject types. (Textures, AnimationClip, Camera, etc). Serialization happens at the UnityEngine.Object
level. Each UnityEngine.Object
is always serialized as a whole. They can contain references to other UnityEngine.Objects
, and those references get serialized properly.
MonoBehaviour
components, which are backed by your scripts, also use serlialization. Because of the very high performance requirements that the serializer has, it does not always behave exactly like a C# developer would expect from a serializer. Below is some guidance on hwo to make best use of serliaization.
Ensure it:
public
, or have [SerializeField]
attributestatic
const
readonly
fieldtype
tht is of a type that can be serialized (See below.)[Serializable]
attribute.[Serializable]
attribute. (Added in Unity 4.5.)UnityEngine.Object
.int
, float
, double
, bool
, string
, etc.).List
<T
> of a fieldtype that can be serialized.Custom classes behave like structs
[Serializable]
class Animal
{
public string name;
}
class MyScript : MonoBehaviour
{
public Animal[] animals;
}
If you populate the animals array with three references to a single Animal GameObject, in the serialization stream, you find 3 GameObjects. when it’s deserialized, there are now three different GameObjects. If you need to serialize a complex GameObject graph with references, you cannot rely on Unity’s serializer doing that all automagically for you, and have to do some work to get that GameObject graph serialized yourself. See the example below on how to serialize things Unity doesn’t serialize by itself.
Note that this is only true for custom classes, as they are serialized “inline”, because their data becomes part of the complete serialization data for the MonoBehaviour they are used in. When you have fields that have a reference to something that is a UnityEngine.Object
derived class, like a public Camera myCamera
, the data from that camera are not serialized inline, and an actual reference to the camera UnityEngine.Object
is serialized.
null
for custom classesConsider how many allocations are made when deserializing a MonoBehaviour
that uses the following script.
class Test : MonoBehaviour
{
public Trouble t;
}
[Serializable]
class Trouble
{
public Trouble t1;
public Trouble t2;
public Trouble t3;
}
It wouldn’t be strange to expect 1 allocation. That of the Test GameObject. It also wouldn’t be strange to expect 2 allocations. One for the Test GameObject, one for a Trouble GameObject. The correct answer is 729. The serializer does not support null. If it serializes a GameObject, and a field is null, we just instantiate a new GameObject of that type, and serialize that. Obviously this could lead to infinite cycles, so we have a relatively magical depth limit of 7 levels. At that point we just stop serializing fields that have types of custom classes/structs, lists and arrays.
Since so many of our subsystems build on top of the serialization system, this unexpectedly large serialization stream for the Test MonoBehaviour causes all these subsystems to perform more slowly than necessary. When we investigate performance problems in customer projects, almost always do we find this problem. We added a warning for this situation in Unity 4.5.
Since so many of Unity’s subsystems build on top of the serialization system, this unexpectedly large serialization stream for the Test MonoBehaviour
causes all these subsystems to perform more slowly than necessary.
NOTE: This causes so significant a performance problem in many projects that it carries a warning message (since Unity 4.5).
if you have a public Animal[] animals
and you put in an instance of a dog, a cat and a giraffe, after serialization, you have three instances of Animal.
One way to deal with this limitation is to realize that it only applies to custom classes, which get serialized inline. References to other UnityEngine.Objects
get serialized as actual references, and for those, polymorphism does actually work. You would make a ScriptableObject
derived class or another MonoBehaviour
derived class, and reference that. The downside of this is that you need to store that Monobehaviour
or scriptable GameObject somewhere, and that you cannot serialize it inline efficiently.
The reason for these limitations is that one of the core foundations of the serialization system is that the layout of the datastream for a GameObject is known ahead of time; it depends on the types of the fields of the class, rather than what happens to be stored inside the fields.
Serializing someting that that Unity’s serializer doesn’t support
In many cases the best approach is to use serialization callbacks. These allow you to be notified before the serializer reads data from your fields and after it has finished writing to them. You can use serialization callbacks to have a different representation of your hard-to-serialize data at run time to its representation when you actually serialize.
Transform your data into something Unity understands right before Unity wants to serialize it. Then, right after Unity has written the data to your fields you can transform the serialized form back into the form you want to have your data in at run time
For example: Suppose you want to have a tree data structure. If you let Unity directly serialize the data structure, the “no support for null” limitation would cause your data stream to become very big, leading to performance degradations in many systems:
using UnityEngine;
using System.Collections.Generic;
using System;
public class VerySlowBehaviourDoNotDoThis : MonoBehaviour
{
[Serializable]
public class Node
{
public string interestingValue = "value";
//The field below is what makes the serialization data become huge because
//it introduces a 'class cycle'.
public List<Node> children = new List<Node>();
}
//this gets serialized
public Node root = new Node();
void OnGUI()
{
Display (root);
}
void Display(Node node)
{
GUILayout.Label ("Value: ");
node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));
GUILayout.BeginHorizontal ();
GUILayout.Space (20);
GUILayout.BeginVertical ();
foreach (var child in node.children)
Display (child);
if (GUILayout.Button ("Add child"))
node.children.Add (new Node ());
GUILayout.EndVertical ();
GUILayout.EndHorizontal ();
}
}
Instead, you tell Unity not to serialize the tree directly, and you make a seperate field to store the tree in a serialized format, suited to Unity’s serializer:
using UnityEngine;
using System.Collections.Generic;
using System;
public class BehaviourWithTree : MonoBehaviour, ISerializationCallbackReceiver
{
//node class that is used at runtime
public class Node
{
public string interestingValue = "value";
public List<Node> children = new List<Node>();
}
//node class that we use for serialization
[Serializable]
public struct SerializableNode
{
public string interestingValue;
public int childCount;
public int indexOfFirstChild;
}
//the root of what we use at runtime. not serialized.
Node root = new Node();
//the field we give Unity to serialize.
public List<SerializableNode> serializedNodes;
public void OnBeforeSerialize()
{
//Unity is about to read the serializedNodes field's contents. lets make sure
//we write out the correct data into that field "just in time".
serializedNodes.Clear();
AddNodeToSerializedNodes(root);
}
void AddNodeToSerializedNodes(Node n)
{
var serializedNode = new SerializableNode () {
interestingValue = n.interestingValue,
childCount = n.children.Count,
indexOfFirstChild = serializedNodes.Count+1
};
serializedNodes.Add (serializedNode);
foreach (var child in n.children)
AddNodeToSerializedNodes (child);
}
public void OnAfterDeserialize()
{
//Unity has just written new data into the serializedNodes field.
//let's populate our actual runtime data with those new values.
if (serializedNodes.Count > 0)
root = ReadNodeFromSerializedNodes (0);
else
root = new Node ();
}
Node ReadNodeFromSerializedNodes(int index)
{
var serializedNode = serializedNodes [index];
var children = new List<Node> ();
for(int i=0; i!= serializedNode.childCount; i++)
children.Add(ReadNodeFromSerializedNodes(serializedNode.indexOfFirstChild + i));
return new Node() {
interestingValue = serializedNode.interestingValue,
children = children
};
}
void OnGUI()
{
Display (root);
}
void Display(Node node)
{
GUILayout.Label ("Value: ");
node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));
GUILayout.BeginHorizontal ();
GUILayout.Space (20);
GUILayout.BeginVertical ();
foreach (var child in node.children)
Display (child);
if (GUILayout.Button ("Add child"))
node.children.Add (new Node ());
GUILayout.EndVertical ();
GUILayout.EndHorizontal ();
}
}
Beware that the serializer, including these callbacks coming from the serializer usually happen not on the main thread, so you are very limited in what you can do in terms of invoking Unity API. You can however do the necessary data transformations to get your data from a non-Unity-serializer-friendly format to a Unity-serializer-friendly-format.
When scripts call the Unity API from constructors or field initializers, or during deserialization (loading), they can trigger errors. This section demonstrates the poor practise that causes these errors.
You should call the majority of the Unity API from the main thread; for example, from Start
or Update
on MonoBehaviour.
You should only call a subset of the Unity API from script constructors or field initializers; Debug.Log
or Mathf
for example. This is because constructors are invoked when constructing an instance of a class during deserialization, and these should only run on a main thread but might end up running on a non-main thread. So, you get errors if you call all the Unity API from script constructors or field initializers.
When Unity creates an instance of a MonoBehaviour or ScriptableObject derived class, it calls the default constructor to create the managed GameObject. This happens before entering the main loop, and before the Scene has been fully loaded. Field initializers are also called from the default constructor of a managed GameObject. In general, do not call the Unity API from a constructor, as this is unsafe for the majority of the Unity API.
Examples of poor practice:
//NOTE: THIS IS A DELIBERATE BAD EXAMPLE TO DEMONSTRATE POOR PRACTISE - DO NOT REUSE
public class FieldAPICallBehaviour : MonoBehaviour
{
public GameObject foo = GameObject.Find("foo"); // This line generates an error
// message as it should not be called from within a constructor
}
//NOTE: THIS IS A BAD EXAMPLE TO DEMONSTRATE POOR PRACTISE - DO NOT REUSE
public class ConstructorAPICallBehaviour : MonoBehaviour
{
ConstructorAPICallBehaviour()
{
GameObject.Find("foo"); // This line generates an error message
// as it should not be called from within a constructor
}
}
Both these cases generate the error message: “Find is not allowed to be called from a MonoBehaviour constructor (or instance field initializer), call in in Awake or Start instead.”
Fix this by making the the call to the Unity API in MonoBehaviour.Start
.
When Unity loads a Scene, it recreates the managed GameObjects from the saved Scene and populates them with the saved values (deserializing). In order to create the managed GameObjects, call the default constructor for the GameObjects. If a field referencing a GameObject is saved (serialized) and the GameObject default constructor calls the Unity API, you get an error when loading the Scene. As with the previous error, it is not yet in the main loop and the Scene is not fully loaded. This is considered unsafe for the majority of the Unity API.
Example of poor practice:
//NOTE: THIS IS A BAD EXAMPLE TO DEMONSTRATE POOR PRACTISE - DO NOT REUSE
public class SerializationAPICallBehaviour : MonoBehaviour
{
[System.Serializable]
public class CallAPI
{
public CallAPI()
{
GameObject.Find("foo"); // This line generates an error message
// as it should not be called during serialization
}
}
CallAPI callAPI;
}
This generates the error: “Find is not allowed to be called during serialization, call it from Awake or Start instead.”
To fix this, refactor your code so that no Unity API calls are made in any constructors for any serialized GameObjects. If you need to call the Unity API for a GameObject, do this in the main thread from one of the MonoBehaviour callbacks, such as Start
, Awake
or Update
.