EDIT: I’ve released my VFW (free) with tons of features, pretty much a ShowEmAll on steroids
As it is common when working around Unity’s limitations, the solution is mostly a hack taking advantage of the new ISerializationCallback interface that’s been added recently.
We can write our own serialization system to serialize only the things that Unity doesn’t. Mainly: interfaces, generic types, (1)auto-properties and (2)abstract System.Object classes
- I said auto-properties because it doesn’t make much sense to serialize a property with a backing field. If you wanted to do that, just serialize the backing field instead.
- Unity does serialize abstract classes, but only if they inherit
UnityEngine.Objects
(like MonoBehaviours)
The core logic is (I will explain interfaces - serializing auto-props, generics etc is very similar):
- Given a MonoBehaviour we get all the fields/properties that Unity can’t serialize (in our example, interfaces)
- To make the interface field/property survive an assembly reload, we check the actual implementer of that interface, if it’s a UnityEngine.Object (mostly a MonoBehaviour) we cast it down to a UnityEngine.Object and store the reference in a serialized dictionary or a list.
- Otherwise if the implementer is not a UnityEngine.Object, we have to serialize it manually. (We’ll use BinaryFormatter for the sake of simplicity)
- Great, but what if the interface contains a UnityEngine.Object reference, like a Transform or something? How will the serializer know how to serialize UnityObjects? - The answer is that it doesn’t and can’t know. So we write ‘surrogates’ for those Unity objects (Vector3, Vector2, Transform, Rect, etc) - This might sound like a very tedious job, but it’s not, bare with me.
So let’s start writing our new “SerializedBehaviour” which will act as a standard to inherit from instead of MonoBehaviour:
public abstract class SerializedBehaviour : MonoBehaviour, ISerializationCallbackReceiver
{
public void OnAfterDeserialize()
{
Deserialize();
}
public void OnBeforeSerialize()
{
Serialize();
}
private void Serialize()
{
}
private void Deserialize()
{
}
}
Let’s implement those suckers!
public abstract class SerializedBehaviour : MonoBehaviour, ISerializationCallbackReceiver
{
private Dictionary<string, UnityObject> serializedObjects = new Dictionary<string, UnityObject>();
private Dictionary<string, string> serializedStrings = new Dictionary<string, string>();
private BinaryFormatter serializer = new BinaryFormatter();
public void OnAfterDeserialize()
{
Deserialize();
}
public void OnBeforeSerialize()
{
Serialize();
}
private void Serialize()
{
foreach (var field in GetInterfaces())
{
var value = field.GetValue(this);
if (value == null)
continue;
string name = field.Name;
var obj = value as UnityObject;
if (obj != null) // the implementor is a UnityEngine.Object
{
serializedObjects[name] = obj; // using the field's name as a key because you can't have two fields with the same name
}
else
{
// try to serialize the interface to a string and store the result in our other dictionary
using (var stream = new MemoryStream())
{
serializer.Serialize(stream, value);
stream.Flush();
serializedObjects.Remove(name); // it could happen that the field might end up in both the dictionaries, ex when you change the implementation of the interface to use a System.Object instead of a UnityObject
serializedStrings[name] = Convert.ToBase64String(stream.ToArray());
}
}
}
}
private void Deserialize()
{
foreach (var field in GetInterfaces())
{
object result = null;
string name = field.Name;
// Try and fetch the field's serialized value
UnityObject obj;
if (serializedObjects.TryGetValue(name, out obj)) // if the implementor is a UnityObject, then we just fetch the value from our dictionary as the result
{
result = obj;
}
else // otherwise, get it from our other dictionary
{
string serializedString;
if (serializedStrings.TryGetValue(name, out serializedString))
{
// deserialize the string back to the original object
byte[] bytes = Convert.FromBase64String(serializedString);
using (var stream = new MemoryStream(bytes))
result = serializer.Deserialize(stream);
}
}
field.SetValue(this, result);
}
}
private IEnumerable<FieldInfo> GetInterfaces()
{
return GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(f => !f.IsDefined<HideInInspector>() && (f.IsPublic || f.IsDefined<SerializeField>()))
.Where(f => f.FieldType.IsInterface);
}
}
Now we could write:
public class SerializationTest : SerializedBehaviour
{
public ITestInterface test;
}
public interface ITestInterface
{
string StringValue { get; set; }
float FloatValue { get; set; }
}
[Serializable] // don't forget to add this attribute if you're using BinaryFormatter
public class SystemImplementer : ITestInterface
{
public string StringValue { get; set; }
public float FloatValue { get; set; }
}
public class UnityImplementer : MonoBehaviour, ITestInterface
{
public string StringValue { get; set; }
public float FloatValue { get; set; }
}
Just to test things out in-editor and see if the values really persist/survive assembly reload:
[CustomEditor(typeof(SerializationTest))]
public class SerializationTestEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
var typedTarget = target as SerializationTest;
if (GUILayout.Button("Set to system implementor"))
typedTarget.test = new SystemImplementer();
if (GUILayout.Button("Set to unity implementor"))
typedTarget.test = UnityEngine.Object.FindObjectOfType<UnityImplementer>() ?? new GameObject().AddComponent<UnityImplementer>();
if (GUILayout.Button("Print value"))
Debug.Log(typedTarget.test);
}
}
Now do the following:
- Create an empty game object with
SerializationTest attached (you should see the custom editor buttons)
- Assign the test field to the system implementer or unity implementer (via the buttons)
- Enter play mode, and press the print value button
What did you expect? Of course it doesn’t work, because we’re using naked Dictionary<,>
we need some serializable dictionaries (while writing and testing this, I tried using lists with keys/values but it’s not so great) - Grab this dictionary implementation from here to move on.
First let’s create the dictionaries we need:
[Serializable]
public class StrObjDict : KVPListsDictionary<string, UnityObject>
{
}
[Serializable]
public class StrStrDict : KVPListsDictionary<string, string>
{
}
Now replace the two Dictionary<,>
ies in our SerializedBehaviour with:
[SerializeField] private StrObjDict serializedObjects = new StrObjDict();
[SerializeField] private StrStrDict serializedStrings = new StrStrDict();
Do the same experiment, enter play mode and if everything worked out well, the interface value should persist! Now that’s pretty metal!
But we have a problem, try and add a Vector3
or Transform
field to our test interface, things will not serialize! The problem is that BinaryFormatter
can’t serialize classes not marked up with Serializable
- But what to do if we can’t touch Unity’s code? The solution is ‘surrogates’, add the following surrogate (anywhere)
public class Vector3Surrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
var vector = (Vector3)obj;
info.AddValue("x", vector.x);
info.AddValue("y", vector.y);
info.AddValue("z", vector.z);
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
Func<string, float> get = name => (float)info.GetValue(name, typeof(float));
return new Vector3(get("x"), get("y"), get("z"));
}
}
This should handle serializing Vector3s, now let’s modify our serializer code a bit to tell it to use this surrogate - change the declaration of our serializer to be:
private BinaryFormatter mSerializer; // don't instantiate here
private BinaryFormatter serializer
{
get
{
if (mSerializer == null)
{
mSerializer = new BinaryFormatter();
var selector = new SurrogateSelector();
Action<Type, ISerializationSurrogate> addSurrogate = (type, surrogate) =>
selector.AddSurrogate(type, new StreamingContext(), surrogate);
addSurrogate(typeof(Vector3), new Vector3Surrogate());
// add more custom surrogates here
serializer.SurrogateSelector = selector;
}
return mSerializer;
}
}
Now our serializer knows how to seiralize Vector3s. But wait, what about the rest of the Unity arsenal? Transform, GameObject, etc? Well, here’s where hacking comes in, we’ll write only ‘one’ surrogate for ‘all’ UnityEngine.Objects!
public class UnityObjectSurrogate : ISerializationSurrogate
{
private StrObjDict serializedObjects;
public UnityObjectSurrogate(StrObjDict serializedObjects)
{
this.serializedObjects = serializedObjects;
}
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
string fieldName = context.Context as string;
serializedObjects[fieldName] = obj as UnityObject;
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
string fieldName = context.Context as string;
return serializedObjects[fieldName];
}
}
Back to our serializer code, change the getter to:
private BinaryFormatter serializer
{
get
{
if (mSerializer == null)
{
mSerializer = new BinaryFormatter();
var selector = new SurrogateSelector();
Action<Type, ISerializationSurrogate> addSurrogate = (type, surrogate) =>
selector.AddSurrogate(type, new StreamingContext(), surrogate);
addSurrogate(typeof(Vector3), new Vector3Surrogate());
// add more custom surrogates here
// create our unity surrogate
var unitySurrogate = new UnityObjectSurrogate(serializedObjects);
// get all unity object types
var unityTypes = typeof(UnityObject).Assembly.GetTypes()
.Where(t => typeof(UnityObject).IsAssignableFrom(t))
.ToArray();
// add our surrogate to let the serializer use it to handle unity objects serialization
foreach (var t in unityTypes)
addSurrogate(t, unitySurrogate);
serializer.SurrogateSelector = selector;
}
return mSerializer;
}
}
Now you can have UnityObject references inside your interfaces and things will work just fine!
Here’s the full code for the demo.
But please don’t use this implementation, this was quick and dirty. I tried to make it as simple and easy to understand as I can, it was just for demo purposes, It doesn’t cover serializing properties nor generics. For that, use the implementation in my VFW Linked here
Stay brutal! \m/