Over and over I’ve seen posts around the web and on the various Q&A sites like this:
I have an abstract class and two (or more) classes that implement this abstract class (not MonoBehaviours). All classes are marked Serializable, and I use them in a MonoBehaviour. But for some reason the values in these classes don’t serialize! When I reload my scene or the Unity Editor the fields are empty! How can I have Unity serialize abstract classes?
The problem is when Unity serializes members in a class it doesn’t include the full type information. Therefore it is not possible for it to deserialize abstract classes. At least not directly.
I’ve run into this problem in the past and usually the answer is: don’t do that. Don’t try to serialize abstract classes, or interfaces, or generics, etc. (my past writeup about serializing interfaces is quite different since in that case it was for interfaces on MonoBehaviours).
But today I ran into this problem again and it would greatly complicate my architecture if I couldn’t use abstract classes. I needed a way to serialize a list of objects with an abstract type (of course the individual objects have a concrete, serializable type). This list field is in an ScriptableObject. I tried a couple solutions until I found one that worked for my project.
Big caveat: I’ve only recently started working with this and there may be side effects I have yet to discover, but so far it’s working well for my project. Also I’m using this for ScriptableObjects and prefabs. I’m not sure of the effectiveness of this technique on scene objects - at the very least, references to scene objects may be lost.
Now with that out of the way, the first step to resolving this conundrum is Unity’s ISerializationCallbackReceiver interface. This interface adds two methods to your class:
- OnBeforeSerialize() - Called by Unity just before serializing the Unity Object
- OnAfterDeserialize() - Called by Unity after loading the Unity Object and all serialized fields are populated
So now we can hook into Unity’s serialization mechanism. We could simply create new variables to store the unserializable fields, and populate them during OnBeforeSerialize() and restore them in OnAfterDeserialize(). Unity’s documentation demonstrates this for serializing Dictionaries, which normally can’t be serialized directly by Unity.
But wait, what kind of field can store the abstract class objects? You can’t serialize the base abstract class.
My first attempt was to implement a C#-based serialization mechanism directly. The basic process is to create a MemoryStream and a BinaryFormatter, then use them together to serialize an arbitrary C# object into a byte array:
/// Implemented as extensions so they can be used like this:
/// byte[] data = myObject.Serialize();
/// var deserializedObject = (MyType)data.Deserialize();
public static byte[] Serialize(this object obj) {
MemoryStream ms = null;
try {
ms = new MemoryStream();
var bf = new BinaryFormatter();
bf.Serialize(ms, obj);
return ms.ToArray();
}
finally {
if (ms != null) {
ms.Dispose();
}
}
}
public static object Deserialize(this byte[] data){
using (var ms = new MemoryStream(data)) {
var bf = new BinaryFormatter();
return bf.Deserialize(ms);
}
}
The results can then be dumped into a byte array in the ScriptableObject or MonoBehaviour that is being serialized by Unity.
BUT there is a major downside to this technique: it knows nothing about Unity or Unity’s object relationship mechanisms. If you have member fields with things like Material or reference to other MonoBehaviours and ScriptableObjects this serializer will try to serialize those entire objects, when what we really want is to save references to them!
It turns out Unity has a serialization mechanism that is available through scripting and can almost properly handle references to Unity Objects: JsonUtility.
JsonUtility uses Unity’s built in serialization mechanism and returns the raw results as a JSON string. But it does not serialize the type. We still have to do that ourselves. First I defined a new class to store the combined type information with the JSON data:
[Serializable]
public class SerializedData {
public Type type;
public string data;
public static SerializedData Serialize(object obj) {
var result = new SerializedData() {
type=obj.GetType(),
data = JsonUtility.ToJson(obj)
}
return result;
}
//Returns as an object, which can then be cast
public static object Deserialize(SerializedData sd) {
return JsonUtility.FromJson(sd.data, sd.type);
}
}
Next I used it in my ScriptableObject:
public MyScriptableObj : ScriptableObject, ISerializationCallbackReceiver {
private List<AMyAbstractClass> manyThings;
[SerializeField]
private List<SerializeData> manyThingsSerialized;
// various other code things here...
public void OnBeforeSerialize() {
if(manyThings==null)
return;
manyThingsSerialized = new List<SerializedData>();
foreach(var ac in manyThings) {
manyThingsSerialized.Add(SerializedData.Serialize(ac);
}
}
public void OnAfterDeserialize() {
if(manyThingsSerialized==null)
return;
manyThings = new List<AMyAbstractClass>();
foreach(var serialized in manyThingsSerialized) {
var ac = (AMyAbstractClass)SerializedData.Deserialize(serialized);
manyThingsSerialized.Add(ac);
}
}
}
At this point everything seemed to work great! One of life’s great challenges, serializing abstract classes in Unity, solved! Huzzah!
But then, disaster: I discovered that my Unity Object reference field that I wanted to serialize as part of the abstract class was just storing an object instance ID of 0. Therefore, when it goes to deserialize, the reference to the Unity Object is null!
At this point I was ready to throw my computer in the ocean.
But, no, I had come to far to stop now.
- I can serialize abstract classes in a way that didn’t try to serialize Unity objects whole
- That serialized state is being saved, with type information, in the containing MonoBehaviour/ScriptableObject
- But any references to Unity objects (prefabs and ScriptableObjects in my case) seemed to be lost
But I did have one thing in my favor: the Unity objects I’m references are all project assets/prefabs/ScriptableObjects and not scene objects. So it’s pretty trivial to just pull out the asset GUID using AssetDatabase:
SomeOtherScriptableObject myUnityAsset;
...
var path = AssetDatabase.GetAssetPath(myUnityAsset);
var assetGuid = AssetDatabase.AssetPathToGUID(path);
or going the other way:
public string assetGuid; //make this the serialized field
...
var path = AssetDatabase.GUIDToAssetPath(assetGuid);
myUnityAsset=AssetDatabase.LoadAssetAtPath<SomeOtherScriptableObject>(path);
But that’s pretty complicated. I have to do that for every Unity Object type I want to serialize a reference to? Apparently yes. But C# properties can help lessen the pain. Make a property for the Unity type that internally tracks the GUID string, and load or save that GUID string when necessary.
[HideInInspector]
public string assetGuid;
private SomeOtherScriptableObject _asset;
public SomeOtherScriptableObject Asset {
get {
if(_asset==null) {
#if UNITY_EDITOR
var path = AssetDatabase.GUIDToAssetPath(assetGuid);
_asset=AssetDatabase.LoadAssetAtPath<SomeOtherScriptableObject>(path);
#endif
}
return _asset;
}
set {
_asset = value;
#if UNITY_EDITOR
var path = AssetDatabase.GetAssetPath(_asset);
assetGuid = AssetDatabase.AssetPathToGUID(path);
#endif
}
}
Is it ugly? Yes. But it does work. Fortunately for my particular use case I only have two Unity Object fields in my set of classes (6 so far and growing now that I can serialize them) that were affected by this serialization issue.
One possible issue is properties aren’t exposed in the Inspector, so you won’t be able to update the Asset/_asset value in the Inspector. For my project that was irrelevant because I have a custom inspector, but it could add yet another complication for some implementations.
Summary #
- Create a class that maps type to JSON string representation
- Add ISerializationCallbackReceiver to the ScriptableObject or MonoBehaviour Unity is serializing
- Implement OnBeforSerialize / OnAfterDeserialize
- If necessary, get / set the asset GUID of any Unity Objects (ScriptableObjects, MonoBehaviours, Materials, etc.)