How UnitySerializer Works


This page is NOT complete.

UnitySerializer is based on my SilverlightSerializer – modified to work with Unity components.

It would have been possible to build this serializer around BinaryFormatter – however, due to the need for injection into the serialization process to handle certain Unity components I chose to base it off my existing work which has been included in many different packages and downloaded directly more than 4,000 times.

There are many ways to extend the serialization process to handle other component types, this page will be the source of documentation for those.

One important example of this is the use of “Specialists”.  Currently only one specialist exists – TimeAsFloat.  This specialist can be applied to a float variable that contains a Time.time reference so that on deserialization the time is updated relative to the current system time.

You apply TimeAsFloat like this:


[Specialist(TimeAsFloat)]
public float timeThatSomethingShouldHappen;

Using DeserializeInto

The way that Unity works means that serializing Components requires two different tactics depending on whether you are trying to serialize the data or you are serializing a reference.  Take for example AnimationState – I understand other packages have had problems serializing this component and indeed it isn’t straightforward.  The problem is that Unity creates the animation states associated with a particular game object – you can’t just make one up on your own.  Also if you dynamically load animations into a game object then you have a further problem of being sure about the consistency of the order of the said animations at any time.

UnitySerializer enables the different treatment of a components when they are being stored as children of an object being serialized compared to when they are a reference or the children of some embedded reference.  It does this by using Custom Serializers and a routine that allows for the deserialization of properties and fields into an existing instance, this is called DeserializeInto.

You can add your own custom serializers to handle circumstances not covered by the default code.

[ComponentSerializerFor(typeof(Animation))]
public class SerializeAnimations : IComponentSerializer
{
	public class StoredState
	{
		public string name;
		public byte[] data;
	}
	
	#region IComponentSerializer implementation
	public byte[] Serialize (Component component)
	{
		return UnitySerializer.Serialize( ((Animation)component).Cast<AnimationState>().Select(a=> new StoredState() { data =  UnitySerializer.SerializeForDeserializeInto(a), name = a.name}).ToList() );
	}




	public void Deserialize (byte[] data, Component instance)
	{
		var animation = (Animation)instance;
		var list = UnitySerializer.Deserialize<List<StoredState>>(data);
		foreach(var entry in list)
		{
			UnitySerializer.DeserializeInto(entry.data, animation[entry.name]);
		
		}
		
		
	}
	#endregion
}

Here you can see the code for serializing the animations of a game object – the class is decorated with a ComponentSerializerFor attribute and implements the IComponentSerializer interface. You can see how I use the name of the animation rather than an ordinal position and allow Unity to create the animation states then deserialize into those instances rather than replacing them.

Writing a Specialist

You write a specialist if you want to control how particular variable is serialized. As mentioned above, a classic example is saving a float that actually represents a time. Here is how TimeAsFloat is declared:


[SpecialistProvider]
public class TimeAsFloat : ISpecialist
{
	#region ISpecialist implementation
	public object Serialize (object value)
	{
		return (float)value - Time.time;
	}

	public object Deserialize (object value)
	{
		return Time.time + (float)value;
	}
	#endregion

}

As you can see, you just get direct access to the value being stored and restored – you can then modify it however makes sense. TimeAsFloat just uses the current Unity system Time.time to create an offset which is recreated when the object is loaded.

Using Deferred Deserialization

A number of things that you serialize need to be actually set into the object quite a lot later – for example the position of an object needs to be set after the parent->child relationships have been re-established. To have something be deserialized later the class that contains it is marked with the DeferredAttribute. The data is stored inline, but on deserialization deferred values are set last, just before the script becomes active.

If you want to modify the serialization of something so that it is deferred then you write a custom serializer and return a DeferredSetter rather than the objects actual value. Here’s the one for Vector3:

[Serializer(typeof(Vector3))]
public class SerializeVector3 : SerializerExtensionBase<Vector3>
{
	public override IEnumerable<object> Save (Vector3 target)
	{
		return new object[] { target.x, target.y, target.z};
	}
	
	public override object Load (object[] data, object instance)
	{
		Radical.Log ("Vector3: {0},{1},{2}",data[0], data[1], data[2]);
		return new UnitySerializer.DeferredSetter(d => {
			return new Vector3((float)data[0], (float)data[1], (float) data[2]);
		}
		);
	}
}

You can see that the save code just stores an array of the values – but the loading code returns a DeferredSetter that contains the lambda function necessary to retrieve the actual data when it becomes time to get the value and store it in the transform or whatever.

  1. Leave a comment

Leave a comment