Epic saga of Unity3D deserialization of arbitrary Objects/Classes

(a summary writeup of the things I tried – based on this Unity blog post: blogs.unity3d.com/2014/06/24/serialization-in-unity/ – to get Unity 4.5 to correctly serialize/deserialize simple object graphs)

UPDATE: I belatedly discovered that EditorUtility.setDirty() is fundamental to Unity; if you have even one single call of this missing, Unity will corrupt data when you save the project. In some of the cases below, where everything was fine EXCEPT on fresh startup, I suspect that was the real problem: a missing setDirty(). I thought this method was cosmetic, but tragically: no.

This is not easy to read, it’s bullet points summarising approximately 2 weeks of testing and deciphering (plus a month or so spent learning-the-hard-way the things that are in the Unity blog post above. Note that the blog post only came out very recently! Until then, you had to discover this stuff by trial and error)

Serializable-thing:

– example on blog won’t work if e.g. your Class is an object graph.
– e.g. class Room { List doors; }. class Door { Door otherDoor; }
– …can’t be serialized. Can’t be stored as a struct, unless you have a GUID per “Door” instance. That’s no surprise, didn’t bother me: This is standard in most serialization systems.
– So we go on a quest to create a GUID…

GUID1:

– class Door : ISerializeCallback { Door otherDoor; public string guid = Guid.NewGuid().ToString(); /* use OnBefore / OnAfter callbacks to store the guid of the other door / find the door from guid */ }

Unity inspector lists:

– if you add an item to the list, 2 things corrupt your data:
1. all existing objects are NOT MOVED, but are CLONED into new list. This breaks our GUID.
2. the new item is a carbon-copy clone of the old – so it ends up with the SAME GUID (ouch)

GUID2:

– class Door { [SerializeField]private string _guid = Guid.NewGuid().ToString(); }
– now the inspector no longer clones the guid … BUT … instead it sets it to null.
– it seems to run the constructor, then use serialization to delete all values it finds

GUID3:

– class Door { private string _guid = Guid.NewGuid().ToString(); public string serializedGuid; /** use serialize callbacks to maintain serializedGuid */ }
– Disable the Editor’s list-renderer: it has at least two major bugs that corrupt data. Use the UnityEditorInternal.ReorderableList.
– NB: ReorderableList – with no customization – does not have either of the two bugs I saw with built-in Inspector’s lists.
– NB: as a bonus, with RL, we can avoid the “run constructor, delete all data, deserialize, reserialize” cycle, and directly create a new, blank instance with a safely generated – FRESH! – Guid.

Ctrl-D in Editor:

– breaks this, because we have no idea if a deserialize event comes from “ctrl-d” or from “read back from disk” (or from a prefab instantiating into the scene).

GUID4:

– class Door { public Room serializedRoom; private string _guid; /** use callbacks to peek into the serialized room, and use the guid as a “local ID within this room” */ }
– Because we’re now leaning on Room (which is a GameObject) to provide part of our GUID, in theory we don’t care about prefab/ctrl-d/etc — the code inside MonoBehaviour will automatically separate-out those cases for us.

Work with everything except prefabs, I think

– eventually gave up, removed the GUID completely, and changed game design to only allow ONE door to connect any pair of rooms. Yes, I was desperate!

THIS WORKS! Until you restart Unity:

– restarting unity causes this to deserialize on a different thread, and now you’re NOT ALLOWED to peek into the serialized room. You cannot use it as a substitute ID. You get crashes at load-time).
– …apart from that, this works fine. We have a solution that worked for ctrl-D, for prefabs.

GUID5:

– class Door { public Room serializedRoom; private bool _isUnityStillOnBackgroundThread; /** use the bool to say “deserialize happened but the data is NOT VALID, DONT RUN ANY METHODS” */ }
– …also use an AutoRun script with [InitializeOnLoad] and “EditorApplication.update += RunOnce;”
– …the autorun script waits for Unity to startup, then goes and Finds all objects, and finds all Rooms, all Doors — and does the fixup.

Fail, but can’t be debugged:

– I have no idea why this failed. It appeared perfect. Simply moving code that worked inside the deserialize (but crashed because it was run on “wrong” thread) out into the post-launc RunOnce method … silently fails. No errors, but no data.
– NB: this is impossible to debug, because we can’t even call ToString() on the deserialize method :(.

Conclusion

Give up. Make 10,000’s of Component instances instead. Waste memory, kill performance (especially at runtime: imagine how bad the AI’s going to be iterating over this!) but hey – IT WILL WORK. Because it leans on the built-in hacks that MonoBehaviour has had for years (which we’ve all been using millions of times a day without realising / appreciating ;)).

2 thoughts on “Epic saga of Unity3D deserialization of arbitrary Objects/Classes

  1. Almo

    I’ve just found this post after hunting around for help with serialization in Unity. I assume you’re the same Adam who posted a comment at the Unity blog about how broken this stuff is. Has anything changed with this? My investigations into the problem aren’t turning up anything useful.

  2. adam Post author

    Nope, still badly broken AFAICT.

    I use MonoBehaviour everywhere – it works, life is too short to reverse-engineer Unity’s broken code, and then back-invent workarounds for the bugs.

    I gave a talk recently on how horrendous Unity Serialization is, and Unity folks are certainly aware of it. My understanding: it will get revisited / thrown-away-and-written-properly once IL2CPP is live (i.e. Unity 5 is the main version).

    Until then … it’s definitely worth reporting bugs you find, and/or documenting the “things I’ve tried, that didn’t work, and how” – my impression is that Unity folks will definitely read them, and help where they can (and make sure it’s better in future versions).

Leave a Reply

Your email address will not be published. Required fields are marked *