Would this be useful to anyone?
I wrote a lightweight de-serializer, because I wanted to be able to load data from simple XML files into some GameObjects I had. It’ll handle Unity Behaviours as if they’re data structures (so it ignores all Unity-specific gunk, but allows you to read in stuff into your Components as if its an object). The only thing I haven’t tested is whether it handles dictionaries.
It’s not hugely feature complete, but I suspect it’ll be a better place to start from than scratch for most people solving the same problem.
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Xml;
namespace UnityExtensions.Xml.Serialization
{
//A simple config XML serializer/deserializer
//Intended mainly for reading to and writing from MonoBehaviours
/*
* This class is very complicated and it needs some explaining
*
* Generic programming in C# means that passing a template to this serializer puts constraints on it.
* Unity makes those contraints impossible to enforce across all the objects you'd need to work on.
*
* Unity requires that no behaviour have a new() function. All behaviours have to be added to a
* GameObject, so that makes a lot of sense.
*
* However, if you're deserializing a straightforwards object, then you need a new() constructor.
* This is self-evident: how else will you create a default object to put parameters into?
*
* This means that, if you want to make a serializer that can handle both objects and Unity behaviours
* you have to either detect them separately and handle them separately, or basically use C# reflection
* to bypass EVERYTHING.
*
* I went with the second option because, although the Reflection code is messy as hell, the recursive
* call flow of the code is easier to visualise. It's easy to create a new serializer for any type because
* the serializer IS the serializer for that template. No detection, simple code flow. All the evil is
* inside the deserialization function, where it should be.
*
* The biggest problem with the code is that no assumptions can be made about the template type in the code.
* C# will throw an error if any type of assumption is made in the code. So T Obj = new T() assumes newable,
* casting to T assumes it is a class and so on. This is why every call is made using reflection.
*
* Also, sticking to C# 3.5 means its impossible to create and cast to generic objects. This is a known
* issue and the way around this is the dynamic keyword introduced in C# 4.0. However, we can't use that, so
* maintaining all serializer instances as objects and using reflection on them works very well.
*
*/
public class UnityXmlSerializer<T> {
public Type type = typeof(T);
public Dictionary<string, FieldInfo> fields;
public Dictionary<string, PropertyInfo> properties;
private static Type SerTypeTemplate = typeof( UnityXmlSerializer<> );
bool UnityType = false;
//Damn, I thought I'd made this static? Never mind...
private Dictionary<Type, object > serializerCache;
//These reflect the main types a serialiser will have to deal with and implement
public enum uXmlSerType
{
unknownType, //Ain't got there yet;
basicType, //TypeCode not an object
enumType, //TypeCode an object and (something else I haven't figure out)
arrayType, //Describes IList
dictionaryType, //Describes dictionary - TODO
objectType, //Is an object and is not any of the other types
unityType, //Is a Unity Component type object which must be on a Game Object
}
public uXmlSerType unitySerType;
//I still don't trsut this not to crap out on me, so for now
public int debug = 0;
//Used publically
public UnityXmlSerializer()
{
Init ();
//Create serializer cache
serializerCache = new Dictionary<Type, object>();
//Add self to cache
serializerCache [type] = this;
}
//Used by the serializer when creating different versions of itself in the child code
public UnityXmlSerializer( Dictionary<Type, object> cache )
{
Init ();
serializerCache = cache;
}
public UnityXmlSerializer( Dictionary<Type, object> cache, int debugLevel )
{
debug = debugLevel;
Init ();
serializerCache = cache;
}
/// <summary>
/// Init this instance.
/// Contains parts of the constructors which are common across all of them
/// </summary>
private void Init() {
if( debug>=1)
Debug.Log ("XMLSerialization Test: Constructor starting on type " + type.Name);
if (Type.GetTypeCode(type) != TypeCode.Object) {
if( type.IsEnum )
unitySerType = uXmlSerType.enumType;
else
unitySerType = uXmlSerType.basicType;
} else {
if(type.GetInterface( "IList") != null )
{
unitySerType = uXmlSerType.arrayType;
}
if(type.GetInterface ( "IDictionary" ) != null )
unitySerType = uXmlSerType.dictionaryType;
if( type.IsEnum )
unitySerType = uXmlSerType.enumType;
//Check fields for descent from Unity
fields = new Dictionary<string, FieldInfo> ();
properties = new Dictionary<string, PropertyInfo> ();
foreach (FieldInfo FI in type.GetFields()) {
if (IsUnityBaseClass (FI.DeclaringType.Name)) {
UnityType = true;
} else {
if(debug>=2)
Debug.Log ("XMLSerialization Test: Constructor: Added field " + FI.Name + " to the dictionary which was declared in " + FI.DeclaringType.Name);
fields.Add (FI.Name, FI);
}
}
foreach (PropertyInfo PI in type.GetProperties ( )) {
//Automatically filter out MonoBehaviour and Object stuff - We only want anything after that.
if (IsUnityBaseClass (PI.DeclaringType.Name)) {
UnityType = true;
} else {
if(debug>=2)
Debug.Log ("XMLSerialization Test: Constructor: Added property " + PI.Name + " to the dictionary which was declared in " + PI.DeclaringType.Name);
properties.Add (PI.Name, PI);
}
}
if( UnityType )
unitySerType = uXmlSerType.unityType;
//If all other options have fallen through
if( unitySerType == uXmlSerType.unknownType )
{
unitySerType = uXmlSerType.objectType;
}
}
//Prints full list of everything there for really hard testing purposes
if (debug >= 4) {
foreach (FieldInfo FI in type.GetFields ()) {
Debug.Log ("XMLSerialization Test: Constructor field list: " + FI.Name);
}
foreach (PropertyInfo PI in type.GetProperties ()) {
Debug.Log ("XMLSerialization Test: Constructor property list: " + PI.Name);
}
foreach (MemberInfo MI in type.GetMembers()) {
Debug.Log ("XMLSerialization Test: Constructor member list: " + MI.Name);
}
}
if (debug >= 3) {
Debug.Log ("XmlSerializer: Constructor: Finishing with type " + type.Name + " which we think is " + unitySerType);
Debug.Log ("XmlSerializer: Constructor: We know that: UnityType - " + UnityType + "; TypeCode - " + Type.GetTypeCode (type) + " and enum - " + type.IsEnum);
}
}
/*public void Serialize( T Object, string name, XmlNode parent )
{
if( debug>=1)
Debug.Log ("XMLSerialization: Serialize");
if (unitySerType == uXmlSerType.unityType || unitySerType == uXmlSerType.objectType) {
if(debug>=2)
Debug.Log ("XMLSerialization: Starting unity or object type deserialization");
XmlNode element;
foreach (KeyValuePair<string,FieldInfo> field in fields) {
object serializer = GetSerializer (field.Value.FieldType);
element = parent.OwnerDocument.CreateNode ("element", field.Key, "" );
parent.AppendChild (element);
object[] objs = new object[] { field.Value.GetValue(), field.Key, element };
serializer.GetType ().GetMethod ("Serialize").Invoke (serializer, objs);
}
foreach (KeyValuePair<string, PropertyInfo> property in properties) {
object serializer = GetSerializer (property.Value.PropertyType);
element = parent.OwnerDocument.CreateNode ("element", property.Key, "" );
parent.AppendChild (element);
object[] objs = new object[] { property.Value.GetValue(), property.Key, element };
serializer.GetType ().GetMethod ("Serialize").Invoke (serializer, objs);
}
} else if (unitySerType == uXmlSerType.basicType) {
if(debug>=2)
Debug.Log ("XMLSerialization: Starting basic type serialization");
MethodInfo convertMethod;
if( Type.GetTypeCode(type) != TypeCode.String )
convertMethod = typeof(XmlConvert).GetMethod ( "ToString", new Type[] {typeof(T)} );
else
convertMethod = typeof(string).GetMethod("Clone");
if( convertMethod == null )
{
//Error
}
else
{
string str;
if( Type.GetTypeCode (type)!= TypeCode.String )
str = (string)convertMethod.Invoke( null, new object[] { Object } );
else
str = (string)convertMethod.Invoke ( null, new object[]{ Object } );
}
}
}*/
public T Deserialize( GameObject GO, XmlNode E )
{
if( debug>=1)
Debug.Log ("XMLSerialization: Deserialize");
//All of this needs way more error checking considering just how much stuff I bypass
T newVar;
if (unitySerType == uXmlSerType.unityType) {
Type goType = GO.GetType ();
MethodInfo castMethod = goType.GetMethod ("AddComponent", new Type[0]).MakeGenericMethod (type);
newVar = (T)castMethod.Invoke (GO, null);
//newVar = GO.AddComponent (typeof(T)) as T;
if( debug>=2 )
Debug.Log ("XMLSerialization: Created unity component");
} else if (unitySerType == uXmlSerType.basicType) {
if(debug>=1)
Debug.Log ("XMLSerialization: Initiallised basic type");
newVar = default(T);
if( Type.GetTypeCode (type) == TypeCode.String )
{
if( newVar == null && debug>=4 )
{
Debug.Log ("So an empty string evaluates to null then?"); //This bit me bad
}
}
if(debug>=3)
Debug.Log ("XMLSerialization Test: Initiallised default object");
//Do nothing
} else if ( unitySerType == uXmlSerType.enumType ){
newVar = default(T);
}
else
{
//Handle subclasses held through an interface
if (E.Attributes != null && E.Attributes ["typeHint"] != null) {
Debug.Log ("Parent class interface, forwarding it to the child serializer");
object serializer = GetSerializer (Type.GetType(E.Attributes["typeHint"].Value));
object[] objs = new object[2] { GO, E };
return (T)serializer.GetType ().GetMethod ("Deserialize").Invoke (serializer, objs);
}
//Because we're calling stuff manually, check if the blank constructor exists
ConstructorInfo ctor = type.GetConstructor (new Type[0]);
if (ctor != null) {
newVar = (T)Activator.CreateInstance (typeof(T)); //Gets around Unity's new() blockade
if(debug>=2)
Debug.Log ("XMLSerialization: Created class instance"); //Super useful to know under some debug circumstances
} else {
Debug.LogError ("XMLSerialization: Deserialize: Unable to find empty contructor for the class you're trying to deserialize. This is bad.");
newVar = default(T);
}
}
//If we get here and have nothing, then push error
//(note: empty strings evaluate to null and always trip this)
if (newVar == null && Type.GetTypeCode (type ) != TypeCode.String ) {
Debug.LogError ("Xml Deserializer: Could not create a new component of type " + type.Name + " in GameObject " + GO.name);
}
if (unitySerType == uXmlSerType.basicType) {
if(debug>=2)
Debug.Log ("XMLSerialization: Starting basic type deserialization");
//We take advantage of the fact that XMLConvert's functions has a naming scheme that fits well with reflective patterns
//Exception to this is ToString, which just doesn't work
MethodInfo convertMethod;
if( Type.GetTypeCode(type) != TypeCode.String )
convertMethod = typeof(XmlConvert).GetMethod ( "To" + type.Name );
else
convertMethod = E.InnerText.GetType().GetMethod("Clone");
if( convertMethod == null )
{
Debug.LogError ( "XmlSerialization: Could not find conversion to specified type " + type.Name + " (we looked for a function To"+type.Name +"() in XmlConvert)" );
}
else
{
if( debug>=3 )
Debug.Log ("Basic conversion about to start" );
if( Type.GetTypeCode (type)!= TypeCode.String )
newVar = (T)convertMethod.Invoke( null, new object[] { E.InnerText } );
else
newVar = (T)convertMethod.Invoke ( E.InnerText, new object[]{} );
if(debug>=4)
Debug.Log ("Basic type conversion went fine.");
}
} else if(unitySerType == uXmlSerType.enumType) {
//MethodInfo convertMethod = typeof(XmlConvert).GetMethod ( "ToInt32" );
//newVar = (T)convertMethod.Invoke (null, new object[]{E.InnerText});
newVar = (T)Enum.Parse ( type, E.InnerText, true );
} else if (unitySerType == uXmlSerType.unityType || unitySerType == uXmlSerType.objectType) {
foreach (XmlNode child in E.ChildNodes) {
if (fields.ContainsKey (child.Name)) {
FieldInfo thisField = fields [child.Name];
object serializer = GetSerializer( thisField.FieldType );
object[] objs = new object[2] { GO, child };
if( debug>=2 )
Debug.Log ("Field " + thisField.Name + " exists in this object " + type.Name + " and we're about to deserialize into it" );
thisField.SetValue (newVar, serializer.GetType ().GetMethod ("Deserialize").Invoke (serializer, objs));
}
if (properties.ContainsKey (child.Name)) {
PropertyInfo thisProp = properties [child.Name];
object serializer = GetSerializer ( thisProp.PropertyType );
object[] objs = new object[2] { GO, child };
thisProp.SetValue (newVar, serializer.GetType ().GetMethod ("Deserialize").Invoke (serializer, objs), null);
}
}
} else if (unitySerType == uXmlSerType.arrayType)
{
if( type.IsArray )
{
Type elmntType = type.GetElementType();
object serializer = GetSerializer(elmntType);
int i=0;
foreach( XmlNode child in E.ChildNodes )
{
i++;
}
int size = i;
object[] tempArray = new object[i];
i=0;
foreach( XmlNode child in E.ChildNodes )
{
object[] objs = new object[2] { GO, child };
object temp = serializer.GetType().GetMethod( "Deserialize").Invoke (serializerCache, objs );
tempArray[i++] = temp;
}
MethodInfo resizeMethod = type.GetMethod ("Resize");
resizeMethod.Invoke ( newVar, new object[] { size } );
IList list = (IList)newVar;
i=0;
foreach( object o in tempArray )
{
list[i++] = o;
}
}
else {
IList list = (IList)newVar;
Type[] typeParams = type.GetGenericArguments();
object serializer=null;
if( list != null || typeParams != null || typeParams.Length == 1 )
{
serializer = GetSerializer( typeParams[0]);
}
else
Debug.LogError ("XMLSerialization: List section: Problem with List generic arguments, there should be one and only one template argument");
foreach( XmlNode child in E.ChildNodes )
{
object[] objs = new object[2] { GO, child };
object temp = serializer.GetType().GetMethod( "Deserialize").Invoke (serializer, objs );
list.Add ( temp );
}
}
} else if(unitySerType == uXmlSerType.dictionaryType )
{
IDictionary dict = (IDictionary)newVar;
Type[] typeParams = type.GetGenericArguments();
object keySerializer = null;
object valueSerializer = null;
if( dict != null || typeParams != null || typeParams.Length == 2 )
{
keySerializer = GetSerializer( typeParams[0]);
valueSerializer = GetSerializer(typeParams[1]);
}
object[] pair =new object[2];
pair[0] = null;
pair[1] = null;
foreach( XmlNode child in E.ChildNodes )
{
if( child.Name == "key" )
{
object[] objs = new object[2] { GO, child };
pair[0] = keySerializer.GetType().GetMethod( "Deserialize").Invoke (keySerializer, objs );
}
if( child.Name == "value" )
{
object[] objs = new object[2] { GO, child };
pair[1] = valueSerializer.GetType().GetMethod( "Deserialize").Invoke (valueSerializer, objs );
}
if( pair[0] != null && pair[1] != null )
{
dict.Add ( pair[0], pair[1] );
//Prevent spurious adding in the event XML was generated elsewhere and includes other fields
pair[0]=null;
pair[1] = null;
}
}
}
if(debug>=1)
Debug.Log ("About to return.");
return newVar;
}
//----Useful helper functions----
//This code fragment is used numerous times
private object GetSerializer( Type _type ) {
object serializer;
if (serializerCache.ContainsKey (_type)) {
serializer = serializerCache [_type];
} else {
Type newSerType = SerTypeTemplate.MakeGenericType (_type);
serializer = Activator.CreateInstance (newSerType, serializerCache );
serializerCache [_type] = serializer;
}
return serializer;
}
private bool IsUnityBaseClass( string className )
{
return ( className == "Object" || className == "Component" || className == "Behaviour" || className == "MonoBehaviour");
}
}
}
Also requires (as a dependency):
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//Call explicitly
namespace UnityExtensions
{
public class Hierarchy {
//Children
public static System.Collections.Generic.IEnumerable<GameObject>
Children( GameObject Parent )
{
//GameObject GO;
for (int i=0; i<Parent.transform.childCount; ++i)
yield return Parent.transform.GetChild (i).gameObject;
}
public static GameObject Parent( GameObject GO )
{
if(GO.transform.parent != null )
return GO.transform.parent.gameObject;
else
return null;
}
public static GameObject FirstChild( GameObject GO )
{
return GO.transform.GetChild (0).gameObject;
}
public static GameObject SecondChild( GameObject GO )
{
return GO.transform.GetChild (1).gameObject;
}
public static GameObject LastChild( GameObject GO )
{
return GO.transform.GetChild ( GO.transform.childCount-1 ).gameObject;
}
//Find
public static GameObject FindChildByName( GameObject GO, string name ) {
foreach ( GameObject child in Hierarchy.Children (GO) )
{
if(child.name == name )
return child;
}
return null;
}
public static GameObject FindDescendantByName( GameObject GO, string name ){
if(GO.name == name )
return GO;
foreach ( GameObject child in Hierarchy.Children (GO) )
{
GameObject recurse = FindDescendantByName(child, name );
if( recurse != null)
return recurse;
}
return null;
}
public static GameObject AddChild( GameObject GO, GameObject Adopted )
{
Adopted.transform.SetParent (GO.transform, true);
return Adopted;
}
public static System.Collections.Generic.IEnumerable<GameObject> Ancestors( GameObject GO ) {
GameObject g = GO;
while (g != null) {
yield return g;
g = Hierarchy.Parent (g);
}
}
public static List<GameObject> Ancestry( GameObject GO )
{
List<GameObject> ancestry = new List<GameObject>();
GameObject g = GO;
while (g != null) {
ancestry.Add (g);
g = Hierarchy.Parent (g);
}
return ancestry;
}
public static GameObject CommonAncestor( GameObject GO1, GameObject GO2 )
{
List<GameObject> list1 = Ancestry (GO1);
List<GameObject> list2 = Ancestry (GO2);
foreach (GameObject G1 in list1)
{
foreach (GameObject G2 in list2)
{
if (G1 == G2)
return G1;
}
}
return null;
}
}
}
//...or add to GameObject's methods through extensions
namespace UnityEngine.Extensions.Hierarchy
{
public static class HierarchyExtensionsWrapper {
//Relations
public static IEnumerable<GameObject> Children ( this GameObject GO )
{
return UnityExtensions.Hierarchy.Children( GO );
}
public static GameObject Parent( this GameObject GO )
{
return UnityExtensions.Hierarchy.Parent(GO);
}
public static GameObject FirstChild( this GameObject GO )
{
return UnityExtensions.Hierarchy.FirstChild(GO);
}
public static GameObject SecondChild( this GameObject GO )
{
return UnityExtensions.Hierarchy.SecondChild (GO);
}
public static GameObject LastChild( this GameObject GO )
{
return UnityExtensions.Hierarchy.LastChild (GO);
}
//Find
public static GameObject FindChildByName( this GameObject GO, string name ) {
return UnityExtensions.Hierarchy.FindChildByName (GO, name);
}
}
}