- Home /
(SOLUTION) How to serialize interfaces, generics, auto-properties and abstract System.Object classes?
So as a programmer I care a lot about my code quality and good coding practices. I like to write clean, easy to understand, well-abstracted, non-cumbersome code. For that, I use many techniques in my arsenal such as interfaces, properties, polymorphism, etc.
Unfortunately Unity doesn't seem to like that. There was always issues with polymorphic serialization, Unity never encouraged the use of interfaces nor properties cause they don't play well with its 'serialization system'. Are you saying that due to Unity's technicalities we can't practice good programming habits and write cool designs?!
Unity is a great engine with a very fluid work-flow, but they always seem to neglect their framework and API from the aspect I mentioned above (supporting interfaces, properties etc) - That has always been the concern of a lot of us for very long times, even since Unity 3.x
With Unity 5 approaching, it's a pity that there's not yet a solution to this problem!
Note to Unity: Please order your priorities well, I mean
"Want a new Mono? Want a better programming framework? Alright, here's an Audio-mixer! :)"
?????
Couldn't flippin' agree more. Workarounds are such a PITA. Semper Fi, brother.
Well that was handful. Took an hour to write and test the demo.
Answer by vexe · Sep 02, 2014 at 02:06 PM
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/
I tried writing my own serializer (which would end up being partially like yours) however I stumbled onto very interesting issue: OnBeforeSerialize
being called like 10 times per second in the inspector (I'm so far ok with that) but when I change something in that inspector window and press Enter, changes do not apply. Even more interesting is at the moment of pressing Enter key the OnAfterDeserialize
(not before as one might think!!) is being called, and at that moment the new data is actually applied onto the object, but of course its getting erased by the internal byte[] array which stores all the serialized data itself.
Hello Sir, @vexe
I have one query regarding List collection. If I am having some kind of transform List like, public List<Transform> gridCells;
Then how could I serialize 'gridCells'....? I want sequence of this list in next game launch....
Thanks in advance. :)
@dinesh-jadhav if you want to write a Transform to disk, you have to write the position/rotation/scale, so you need a function to write a Vector3 and Quaternion to disk. Once you have that, you write a function that writes a List to disk. To save a List to disk you need to first write how many elements in the list, so an Int32 List.Count (4 bytes), and then the elements themselves one by one, in this case you would call in to the previous Transform save function. When loading, you read your list count, populate your list by that much, and now read in the transforms, you would need instances to load the saved values into which is why saving/loading transoforms directly is kind of a pain. So depeneding on what you exactly need, you might not need saving transforms directly
If you're dealing with some sort of Grid and want to save its cells locations, usually if you're dealing with a fixed grid, then every cell has an index. So all you need to really save in order to re-load your grid the next time, is just a List. You could position the cells correctly from those indices assu$$anonymous$$g they're of fixed size.
I m having a List of Transform in which grid of different GameObject's Class are referred and saved. Every grid element has their own values and other attributes. as per your reply i can manage to load the cells with an index. but suppose i want to load that cells with their last saved variable values and all other attributes then how could i do it with serialize......?
Hey, first of all thanks for this amazing concept which I finally can build on. But I'm having some problems with the BinaryFormatter and the Surrogates. The formatter seems to not use the surrogates, even tho I defined them and passed them to the SurrogateSelector etc. Everything works fine until the point I add unity specific types to the interface. I'll always get the error SerializationException: Type 'UnityEngine.Vector3' in Assembly 'UnityEngine.Core$$anonymous$$odule, Version=0.0.0.0, Culture=neutral, Public$$anonymous$$eyToken=null' is not marked as serializable.
I know this thread is pretty old already and you don't want people to meddle with unity's serialization (anymore).. but I really need to be able to keep references in my generic classes. I've tried sooooo many different approaches already, but nothing seems to work properly. I had my hopes really high when I found this solution here, but I just can't get the formatter to (de)serialize the unity types like Vector3 and UnityEngine.Object. Do you know by any chance where that problem might come from? (When I look for the surrogate right before the Serializer.Serialize()
call, it gives me my UnityObjectSurrogate-object just like expected. But the Serialize() call itself throws the error)
Greetings BOTHLine
@BOTHLine I just stumbled upon this looking for a way to serialize classes of different types into one container. (list, dict, array, w/e works).
I ran into an issue trying to group them by a base class, since unity serializes them by their shared base class when I try to access them I get inconsistent data. As in it see$$anonymous$$gly pulls data from random ones and not at the index specified.
Add me on discord if you wanna brainstorm solutions. Full$$anonymous$$e7alJacke7 #4943
In which order do you write/read your container? Is it in a managed way like cycling through a specific set of objects? Or do you use the ISerializationCallbackReceiver? Because the latter one does not use a specific order and is quite random. So if it calls OnBeforeSerialize in the order A, B, C then OnAfterDeserialize doesn't have to be in the same order. and the next time you want to serialize your data it could call B, C, A or whatever. So even that has not to be always the same. That could be the problem with your approach
Answer by winxalex · Oct 24, 2015 at 02:43 PM
You give urself quest and answered. Give us a chance. Not fair. :) First what type is UnityObject? You mean UnityEngine.Object
right? :P I believe that in VFW(understand this was a concept) you not use hardcoded reference "serializedObjects" in Binary Formatter Class. All efforts to have one BinFormater in all code failed and you generated surrogate that only works in SerializedBehaviour and that's it. :P I know you like Dictionaries and like they are more readable so you use "name" strings but serialization is bottle neck, you need speed. For serializedObjects you can use Dict and avoid nasty Convert.ToBase64String(stream.ToArray());
with that you lost all good of having bytearray, instead huge bytearray into string (plus conversion time) by use of Dict<ordNum,byte[]>
. Shhh don't tell anyone. Secret is the Unity will serialize "byte[]". So I came up to more general Surrogate ;) from the constructor idea you have me as Constructor is called once and on a main thread :D
using UnityEngine;
using System.Collections;
using System.Runtime.Serialization;
using System.Linq;
using System;
namespace ws.winx.unity.surrogates{
public class UnityObjectSurrogate : ISerializationSurrogate
{
UnityEngine.Object[] objectsUnity;
public UnityObjectSurrogate(){
Debug.Log ("UnityObjectSurrogate Constructor:"+System.Threading.Thread.CurrentThread.ManagedThreadId);
objectsUnity = UnityEngine.Object.FindObjectsOfType<UnityEngine.Object>();
}
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
var objUnity = (UnityEngine.Object)obj;
Debug.Log ("GetObjectData:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
//Debug.Log("GetObjectData:"+go.name+" ID:"+go.GetInstanceID());
info.AddValue("ID", objUnity.GetInstanceID());
}
/// <summary>
/// Sets the object data.
///
/// !!! Not working cos Main Thread restriction
/// </summary>
/// <returns>The object data.</returns>
/// <param name="obj">Object.</param>
/// <param name="info">Info.</param>
/// <param name="context">Context.</param>
/// <param name="selector">Selector.</param>
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
Debug.Log ("SetObjectData:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
int ID = (int)info.GetValue ("ID", typeof(int));
// Debug.Log ("SetObjectData " + System.Threading.Thread.CurrentThread.ManagedThreadId + "ID:" + ID);
return objectsUnity.FirstOrDefault (itm => itm.GetInstanceID () == ID);
//return null;
}
}
}
Answer by Nit_Ram · Jun 18, 2019 at 04:25 PM
Found this on Google in 2019, so maybe this info might be helpful for others: Odin Inspector can serialize interfaces.
That is true indeed but not everyone has 50 bucks to spend on an asset...
Of course not, but for those who can, my info might be helpful.
You are absolutely right. I do believe this asset to be very nice. $$anonymous$$y apologies for being so blunt.