Serialize a graph or any complex data structure in Unity
Published 9 months ago
Serialize a complex data structure in the editor using the default serialization system
In this article, i will show you how to serialize a graph or any complex data structures with parent or self references as an asset without getting any kind of error. I will use a data structure that i have used in another project (Procedural Worlds Editor) to illustrate this article. You can see in the picture beside the data structure diagram for the graph, more informations are available on github.
The default serialization system in unity has a lot of problems but the most annoying is that it does not support polymorphism so you can't serialize a child class from a serializable parent class. Another big problem is the non-support for null serialization, this is so stupid that if you serialize a null string and deserialize it, the string will be empty and not null (that's you should always use String.IsNullOrEmpty to check for a null in serialized strings). This create another problem: what append if you Instantiate a class and store it's instance in two different serialized variables ? well the response is simple at deserialization you got two different classes :). Some of these problems are solved by ScriptableObjects but this solution require to save the instance of this class saved as an asset in your project which is not really convenient.
For my Graph and Node class i choose to inherit from ScriptableObject so i can use polymorphism on my nodes to customize them and i can save the graph as an asset in my project which is a good thing. Because i need to save as assets each node i choose to add them to the graph file as subassets using AssetDatabase.AddObjectToAsset so no problem about graph and node serialization. For my AnchorFields they are just stored in two lists (one for input and the other for output) inside my nodes, Anchors are stored in a list in my anchorFields and each anchor have a list of Links, for these classes there is no need to inherit from ScriptableObject so i just use the System.Serializable attribute to mark them as serializable.
Then for more convenience in my scripts i decided to add a reference to the graph in each node, a reference of the node in my anchors and in my links a reference of the two connected anchors and nodes. But unity does not support at all self references or parent references and because it does not support neither null serialization, each serialized field have to be instantiated so each node instantiate a graph which instantiate a node which instantiate a graph .... and welcome to the Serialization Depth Limit Exceeded warning message. This is totally stupid but we have to deal with it (or use another serialization system / asset) so as we can't serialize class references i decided to mark all my parent references as System.NonSerializable, it removes the warning but we now have to set our references manually each time the objects are deserialized. To manage this, i decided to use the ISerializationCallbackReceiver interface which let you know when your class will be serialized or have been deserialized. So i implemented it on my Graph class and created a OnDeserialize function on each node, anchorField and anchor which is called by the OnAfterDeserialize method in my graph. But there is a problem here: it's about the asset deserialization order, in unity main asset gets deserialized before their child assets and each of my nodes are actually children of my graph. My nodes are not yet deserialized when their OnDeserialization callback is called from the graph and this is a problem because before the deserialization the Node class is empty (which means no anchorFields) so i can't call OnDeserialize on AnchorFields. Using ISerializationCallbackReceiver will not work because we are not calling the node's OnDeserialize function at the good moment, all we need is a function which is called when the Graph is ready and we actually have one: OnEnable, once the code moved to this function, everything works well and every parent references is valid and ready for a further usage.
Now let's talk a bit about links which are stored inside anchors. To have a clean system i decided to have only one instance of a link which is stored in the two anchors linked and at deserialization, they put the reference of their class inside the link. Because of unity serialization system does not support when a non-ScriptableObject class is serialized from multiple places the link will broke at deserialization (it split in two instances instead of one). So i can't use my Anchor class to store my links, i decided to place them inside a class called AnchorLinkTable (which is stored in my graph class), this class contains a SerializableDictionary which store each link instance and it's associated GUID generated ta the creation of the link by the NewGuid function. Instead of serializing my link instances i choose to store their GUID and when my anchor receive the OnDeserialize callback, they use the linkTable to find the link instance from the GUID.
You can try it out on github, all you have to do is open the Node Editor window and add some nodes, anchors groups in output and input and add some link (they will be randomly link). You can visualize (sorry for disgusting design) beside the graph with all links (one color per link).
So this complete my article about how to serialize a complex data structure, i recommend to read some really interesting articles (serialization megapost, Assets, Object and serialization, script serialization) about unity Serialization system and if possible avoid using it by taking an asset which manage properly serialization (like odin), it will prevent you from having headaches :)

antoine lelievre
Procedural generation developer - Programmer
Chris Roberts
5 days ago
Indie Developer - Other
Hi Antoine. Great article. I'm just wondering if you've tried instantiating graphs you've implemented this way to create unique instances at runtime. I'm working with a graph that needs to be used by multiple game objects that set properties, etc. If I use ScriptableObject.Instantiate, then I just get the root asset, though, and any sub-assets are ignored (it just references the sub-assets from the original). Yours is one of the better descriptions of using ScriptableObject in this way so I was wondering if you'd come across this problem before. Thanks