Using reflection with serialization – Part 2

In previous part I explained a way to serialize object to JSON format using reflection this time we will use reflection to deserialize object using reflection. While much of deserialization can be achieved by simply using the SetValue method the syntax for creating arrays, lists and objects is a bit more complex.

Deserializing object

First we start by creating a static class with method to deserialize object of generic type. Type argument is required as we only included type-names to objects within a collection to support polymorphism. Returning objects instead of known type with methods like this tend to lead to messy patterns with lot’s of ifs or similar to SerializeField in previous article and DeserializeField later in this article.

We will also use Activator to Create a new instance of the given type that we will then fill with the data we have serialized.

using System.Reflection;
using System.Collections.Generic;
using System.Collections;
using SimpleJSON;
using System;

public static class ObjectDeserializer
{
    public static T DeserializeObject<T>(string json)
    {
        JSONNode js = JSONObject.Parse(json);

        Type objectType = typeof(T);
        T newInstance = (T)Activator.CreateInstance(objectType);

        DeserializeObject(newInstance, js);
        ObjectSerializer.SerializeObject(newInstance);

        return newInstance;
    }
}

Just like with Serializing we will get all the fields from the type we are deserializing but in this case it will give us information on fields that have been serialized.

There is however one problem that’s left unresolved here which is the case where objects or their fields get renamed which could potentially lead to loss of data. There are number of ways to fix this but none of them very pretty.

One that is being used by unity is the FormerySerializedAsAttribute which one could check for each field to see if serialized data with the old field name exists. Another one would be to implement a solver of sorts that can detect these changes and fix the serialized data accordingly. Simplest is probably to use text editor like NotePad++ with JSON plugin to browse the serialized data and find and replace all the renamed fields.

static void DeserializeObject(object targetObject, JSONNode data, object rootObject = null)
{
    if (rootObject == null)
        rootObject = targetObject;

    FieldInfo[] fieldInfos = targetObject.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance);

    for (int i = 0; i < fieldInfos.Length; i++)
        DeserializeField(fieldInfos[i], data, rootObject);
}

We can use field type and field name to fetch the matching data from the serialized object which we can then cast to it’s correct type and use the Field.SetValue method to finally set the value of the field.

With arrays we use the Array.CreateInstance method to create the array with the length information we previously serialized with SerializeField. Arrays also have their own Array.SetValue method which we will use before finally setting the value of the field with the array we’ve created.

With Lists we need type that contains the generic type information. Here we create the type using Type.MakeGenericType method. Then we use Activator to create a new list instance with the given type which we cast to IList which we can use to add new items to the list.

When deserializing objects we again use activator to create new instance based on the field type or the serialized type with objects within collection. Then deserialize the object with recursion calling the DeserializeObject again to get the deserialized version of the object to set to field in question.

static void DeserializeField(FieldInfo field, JSONNode data, object rootObject)
{
    Type fieldType = field.FieldType;

    if (fieldType.IsGenericType)
        fieldType = fieldType.GetGenericTypeDefinition();

    if (fieldType.IsArray)
        fieldType = typeof(Array);

    if (fieldType == typeof(int) || fieldType.IsEnum)
    {
        field.SetValue(rootObject, data[field.Name].AsInt);
    }
    else if (fieldType == typeof(float))
    {
        field.SetValue(rootObject, data[field.Name].AsFloat);
    }
    else if (fieldType == typeof(bool))
    {
        field.SetValue(rootObject, data[field.Name].AsBool);
    }
    else if (fieldType == typeof(string))
    {
        field.SetValue(rootObject, data[field.Name].Value);
    }
    else if (fieldType == typeof(Array))
    {
        Type arrayType = field.FieldType;
        int rank = arrayType.GetArrayRank();

        JSONArray jsArray = data[field.Name]["Array"].AsArray;
        Type elementType = arrayType.GetElementType();

        Array arr;

        if (rank == 1)
        {
            int lenght = data[field.Name]["Lenght"].AsInt;
            arr = Array.CreateInstance(elementType, lenght);

            if (elementType.IsPrimitive || elementType == typeof(string))
            {
                if (elementType == typeof(int) || elementType.IsEnum)
                {
                    for (int i = 0; i < arr.Length; i++)
                        arr.SetValue(jsArray[i].AsInt, i);
                }
                else if (elementType == typeof(float))
                {
                    for (int i = 0; i < arr.Length; i++)
                        arr.SetValue(jsArray[i].AsFloat, i);
                }
                else if (elementType == typeof(bool))
                {
                    for (int i = 0; i < arr.Length; i++)
                        arr.SetValue(jsArray[i].AsBool, i);
                }
                else if (elementType == typeof(string))
                {
                    for (int i = 0; i < arr.Length; i++)
                        arr.SetValue(jsArray[i].Value, i);
                }
            }
            else if (elementType.IsClass || (elementType.IsValueType && !elementType.IsEnum))
            {
                for (int i = 0; i < arr.Length; i++)
                {
                    string typeString = jsArray[i]["Type"].Value;
                    Type entryType = Type.GetType(typeString);

                    object o = Activator.CreateInstance(entryType);
                    DeserializeObject(o, jsArray[i]["Value"], o);
                    arr.SetValue(o, i);
                }
            }
            field.SetValue(rootObject, arr);
        }
        else if (rank == 2)
        {
            int lenght1 = data[field.Name]["Lenght1"].AsInt;
            int lenght2 = data[field.Name]["Lenght2"].AsInt;
            arr = Array.CreateInstance(elementType, lenght1, lenght2);

            if (elementType.IsPrimitive || elementType == typeof(string))
            {
                if (elementType == typeof(int) || elementType.IsEnum)
                {
                    for (int i = 0; i < arr.GetLength(0); i++)
                        for (int j = 0; j < arr.GetLength(1); j++)
                            arr.SetValue(jsArray[i].AsInt, i, j);
                }
                else if (elementType == typeof(float))
                {
                    for (int i = 0; i < arr.GetLength(0); i++)
                        for (int j = 0; j < arr.GetLength(1); j++)
                            arr.SetValue(jsArray[i].AsFloat, i, j);
                }
                else if (elementType == typeof(bool))
                {
                    for (int i = 0; i < arr.GetLength(0); i++)
                        for (int j = 0; j < arr.GetLength(1); j++)
                            arr.SetValue(jsArray[i].AsBool, i, j);
                }
                else if (elementType == typeof(string))
                {
                    for (int i = 0; i < arr.GetLength(0); i++)
                        for (int j = 0; j < arr.GetLength(1); j++)
                            arr.SetValue(jsArray[i].Value, i, j);
                }
            }
            else if (elementType.IsClass || (elementType.IsValueType && !elementType.IsEnum))
            {
                for (int i = 0; i < arr.GetLength(0); i++)
                {
                    for (int j = 0; j < arr.GetLength(1); j++)
                    {
                        string typeString = jsArray[i][j]["Type"].Value;
                        Type entryType = Type.GetType(typeString);

                        object o = Activator.CreateInstance(entryType);
                        DeserializeObject(o, jsArray[i][j]["Value"], o);
                        arr.SetValue(o, i, j);
                    }
                }
            }
            field.SetValue(rootObject, arr);
        }
    }
    else if (fieldType == typeof(List<>))
    {
        JSONArray jsArray = data[field.Name].AsArray;
        int entryCount = jsArray.Count;

        Type genericArgument = field.FieldType.GetGenericArguments()[0];
        Type constructedListType = (typeof(List<>).MakeGenericType(genericArgument));
        IList instance = (IList)Activator.CreateInstance(constructedListType);

        if (genericArgument.IsPrimitive || genericArgument == typeof(string))
        {
            if (genericArgument == typeof(int) || genericArgument.IsEnum)
            {
                for (int i = 0; i < entryCount; i++)
                    instance.Add(jsArray[i].AsInt);
            }
            else if (genericArgument == typeof(float))
            {
                for (int i = 0; i < entryCount; i++)
                    instance.Add(jsArray[i].AsFloat);
            }
            else if (genericArgument == typeof(bool))
            {
                for (int i = 0; i < entryCount; i++)
                    instance.Add(jsArray[i].AsBool);
            }
            else if (genericArgument == typeof(string))
            {
                for (int i = 0; i < entryCount; i++)
                    instance.Add(jsArray[i].Value);
            }
        }
        else if (genericArgument.IsClass || (genericArgument.IsValueType && !genericArgument.IsEnum))
        {
            for (int i = 0; i < entryCount; i++)
            {
                string typeString = jsArray[i]["Type"].Value;
                Type entryType = Type.GetType(typeString);

                object o = Activator.CreateInstance(entryType);
                DeserializeObject(o, jsArray[i]["Value"], o);
                instance.Add(o);
            }
        }

        field.SetValue(rootObject, instance);
    }
    else if (fieldType == typeof(object))
    {
        JSONNode objectNode = data[field.Name];
        object newInstance = Activator.CreateInstance(fieldType);
        DeserializeObject(newInstance, data[field.Name].AsObject, newInstance);

        field.SetValue(rootObject, newInstance);
    }
}

Using reflection with serialization – Part 1

Note: we are using SimpleJSON in this article so if you’re using something like Json.Net for Unity it already does much of this for you. However much of the info about reflection here can be useful for many other purposes.

Some time ago I wrote an article about implementing save system with JSON and I thought this would be good time to extend that a bit. While the article still covers the basics of such system it can get a bit laborious for more complex and more adaptive data.

It’s good to understand that in addition to saving player progress, save systems can be used for many other purposes. They can be used with tools made for game and level designers to create quest-lines, configure the games A.I or even add new items or characters to the game. Tools like these are real time savers for projects with multiple people working on them. They also take a lot of work off the shoulders of often already very overworked programmers.

They can even be used in conjunction or to extend the already existing tools found in Editors of Game Engines such as Unity, Unreal or Godot.

Reflection

In the previous article I used explicit and implicit operators to convert objects to JSON and back. Writing such operators for every class you want can eventually get pretty laborious and getting around problems related to polymorphism can lead to less than optimal solutions like having to write factories that create correct class instances based on serialized the data.

By using reflection we can get around a lot of these issues since it allows us to get all the information we need to serialize and deserialize objects. With it we can get information about all the fields object has and the type, name and value of said fields. Using this information we can serialize and deserialize objects of varying types with same single method instead of writing custom operators for each type.

Just like in the previously we’ll be using SimpleJSON to do this. You can find it from the end of the article I mentioned before or from this repository.

Serializing object

Let’s start by creating new static class with public method with target object as parameter and string as return value which should eventually return the serialized JSON string.

If you’re using Visual studio you can also highlight the SerializeObjectFields and press Alt + Enter to generate method for it.

using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using SimpleJSON;
using System;

public static class ObjectSerializer
{
    public static string SerializeObject(object targetObject)
    {
        JSONObject js = new JSONObject();
        SerializeObjectFields(targetObject, js);
        return js.ToString();
    }
}

With reflection we can get the type of the target object and use the GetFields method to obtain information about all the fields it contains. With BindingFlags we can filter what fields get returned. In the example I’ll be using Public and Instance binding flags to return only the public member variables from the class.

We will then pass these fields one by one to a method that serializes them to the JSONNode data object. We will also need the reference to the target object to get field values for serialization.

static void SerializeObjectFields(object targetObject, JSONNode data, object rootObject = null)
{
    if (rootObject == null)
        rootObject = targetObject;

    FieldInfo[] fieldInfos = targetObject.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance);
    for (int i = 0; i < fieldInfos.Length; i++)
        SerializeField(fieldInfos[i], data, rootObject);

}

SerializeField is a lengthy method that basically defines how fields should be serialized based on their type. So far this should support integers, floats, strings, booleans, enums, objects, structs, lists and arrays up to two dimensions. Objects and Structs are handled using recursion by calling the SerializeObjectFields method for each field or element in collection that is either struct or object.

First we start by checking the type of the field whether it’s generic type like list or an array to get type for these without the generic argument or element type so they can be handled separately. Then we get the actual value to serialize as object by using GetValue with reference to the target object.

The basic value types are pretty self-explanatory but with collections we need few extra steps. Firstly we need to know what sort of data the collection contains whether it’s simple types like integers or class instances. Secondly if it’s a class instance we will need to know if it’s inherited type to support polymorphism so we won’t lose data. Thirdly we need to use proper structure for JSONNode data to make it easy to deserialize later.

With arrays one should start by converting the value of the field to an Array and getting its element type from it’s type by using GetElementType method. After this you can get the arrays dimensions using the arrays Rank property. For Arrays of objects I use recursion and call the SerializeObjectFields for each array element passing the element as the new target object and data[field.Name][“Array”][i][“Value”].AsObject as the JSONNode data.

As for the structure I like to store the actual array to data[field.Name][“Array”] and length of the array to data[field.Name][“Lenght”] just to avoid the entry from the Length getting messed up with array values. As for the actual array values I use data[field.Name][“Array”][index] which SimpleJSON will serialize as an array based on the integer index. For objects I use data[field.Name][“Array”][i][“Type”] to store it’s actual type and data[field.Name][“Array”][i][“Value”] to store it’s actual value.

Serializing Lists is pretty similar but instead of GetElemenetType we use GetGenericArguments() method with index zero from the FieldType to get the first GenericArgument. Then we convert the field value to a ICollection which we can use to iterate over the collection in question after which I do pretty much the same as I do with arrays. With lists serializing length isn’t necessary because of their dynamic size.

Note: It’s possible that null-strings or null-entries in objects can cause problems with this. They could however be handled by checking whether field value or entry in the collection is null.

static void SerializeField(FieldInfo field, JSONNode data, object targetObject)
{
    Type fieldType = field.FieldType;

    if (fieldType.IsGenericType)
        fieldType = fieldType.GetGenericTypeDefinition();

    if (fieldType.IsArray)
        fieldType = typeof(Array);

    object fieldValue = field.GetValue(targetObject);

    if (fieldType == typeof(int) || fieldType.IsEnum)
    {
        data[field.Name] = (int)fieldValue;
    }
    else if (fieldType == typeof(float))
    {
        data[field.Name] = (float)fieldValue;
    }
    else if (fieldType == typeof(bool))
    {
        data[field.Name] = (bool)fieldValue;
    }
    else if (fieldType == typeof(string))
    {
        data[field.Name] = (string)fieldValue;
    }
    else if (fieldType == typeof(Array))
    {
        Array arr = (Array)fieldValue;
        Type elementType = arr.GetType().GetElementType();

        if (arr.Rank == 1)
        {
            data[field.Name]["Lenght"].AsInt = arr.GetLength(0);

            if (elementType.IsPrimitive || elementType == typeof(string))
            {
                if (elementType == typeof(int) || elementType.IsEnum)
                {
                    for (int i = 0; i < arr.Length; i++)
                        data[field.Name]["Array"][i] = (int)arr.GetValue(i);
                }
                else if (elementType == typeof(float))
                {
                    for (int i = 0; i < arr.Length; i++)
                        data[field.Name]["Array"][i] = (float)arr.GetValue(i);
                }
                else if (elementType == typeof(bool))
                {
                    for (int i = 0; i < arr.Length; i++)
                        data[field.Name]["Array"][i] = (bool)arr.GetValue(i);
                }
                else if (elementType == typeof(string))
                {
                    for (int i = 0; i < arr.Length; i++)
                        data[field.Name]["Array"][i] = (string)arr.GetValue(i);
                }
            }
            else if (elementType.IsClass || (elementType.IsValueType && !elementType.IsEnum))
            {
                for (int i = 0; i < arr.Length; i++)
                {
                    object item = arr.GetValue(i);
                    data[field.Name]["Array"][i]["Type"] = item.GetType().FullName;
                    SerializeObjectFields(item, data[field.Name]["Array"][i]["Value"].AsObject, item);
                }
            }
        }
        else if (arr.Rank == 2)
        {
            data[field.Name]["Lenght1"].AsInt = arr.GetLength(0);
            data[field.Name]["Lenght2"].AsInt = arr.GetLength(1);

            if (elementType.IsPrimitive || elementType == typeof(string))
            {
                if (elementType == typeof(int) || elementType.IsEnum)
                {
                    for (int i = 0; i < arr.GetLength(0); i++)
                        for (int j = 0; j < arr.GetLength(1); j++)
                            data[field.Name]["Array"][i][j] = (int)arr.GetValue(i, j);
                }
                else if (elementType == typeof(float))
                {
                    for (int i = 0; i < arr.GetLength(0); i++)
                        for (int j = 0; j < arr.GetLength(1); j++)
                            data[field.Name]["Array"][i][j] = (float)arr.GetValue(i, j);
                }
                else if (elementType == typeof(bool))
                {
                    for (int i = 0; i < arr.GetLength(0); i++)
                        for (int j = 0; j < arr.GetLength(1); j++)
                            data[field.Name]["Array"][i][j] = (bool)arr.GetValue(i, j);
                }
                else if (elementType == typeof(string))
                {
                    for (int i = 0; i < arr.GetLength(0); i++)
                        for (int j = 0; j < arr.GetLength(1); j++)
                            data[field.Name]["Array"][i][j] = (string)arr.GetValue(i, j);
                }
            }
            else if (elementType.IsClass || (elementType.IsValueType && !elementType.IsEnum))
            {
                for (int i = 0; i < arr.GetLength(0); i++)
                {
                    for (int j = 0; j < arr.GetLength(1); j++)
                    {
                        object item = arr.GetValue(i, j);
                        data[field.Name]["Array"][i][j]["Type"] = item.GetType().AssemblyQualifiedName;
                        SerializeObjectFields(item, data[field.Name]["Array"][i][j]["Value"].AsObject, item);
                    }
                }
            }
        }
    }
    else if (fieldType == typeof(List<>))
    {
        Type genericArgument = field.FieldType.GetGenericArguments()[0];
        ICollection list = fieldValue as ICollection;

        if (genericArgument.IsPrimitive || genericArgument == typeof(string))
        {
            int i = 0;

            if (genericArgument == typeof(int) || genericArgument.IsEnum)
            {
                foreach (var item in list)
                {
                    data[field.Name][i] = (int)item;
                    i++;
                }
            }
            else if (genericArgument == typeof(float))
            {
                foreach (var item in list)
                {
                    data[field.Name][i] = (float)item;
                    i++;
                }
            }
            else if (genericArgument == typeof(bool))
            {
                foreach (var item in list)
                {
                    data[field.Name][i] = (bool)item;
                    i++;
                }
            }
            else if (genericArgument == typeof(string))
            {
                foreach (var item in list)
                {
                    data[field.Name][i] = (string)item;
                    i++;
                }
            }
        }
        else if (genericArgument.IsClass || (genericArgument.IsValueType && !genericArgument.IsEnum))
        {
            int i = 0;
            foreach (var item in list)
            {
                data[field.Name][i]["Type"] = item.GetType().AssemblyQualifiedName;
                SerializeObjectFields(item, data[field.Name][i]["Value"].AsObject, item);

                i++;
            }
        }
    }
    else
    {
        SerializeObjectFields(fieldValue, data[field.Name]["value"].AsObject, fieldValue);
    }
}

In the next part I’ll be looking at deserializing this object using reflection.

Content size fitter and dynamic content – Part 2

Previously we talked about handling dynamic content using content size fitter and ScrollRects, this time we will be talking about another common type of dynamic content which is text.

There are many forms of text that can vary in length depending on localization or from phrase to another. Now if we were ever to want background for such text like speech bubble or simple black box to make it easier to read it would have to scale along with the content.

The problem here is that Unity draws UI Images in the order they’re in the scene hierarchy so children get drawn over parents. Unfortunately to make use of RectTransforms Stretch feature the black border needs to child of the Text component.

Here are two ways to to get around this.

With Canvas component

Add context size fitter to text component using preferred size for both horizontal and vertical fit. This will make the RectTrasfrom of the text to resize based on the text in text component.

With the background image set RectTransform to Stretch and possibly set some margins using negative values like -10 for left, right, top and bottom. Then add Canvas component and toggle the override sorting option give it a sort order that’s less than the one in parent canvas.

Step 1.
Step 2.

With UIBehavior script

Alternative is to create a new script that inherits from UIBehavior. This allows you to listen for OnRectTransformDimensionsChange events caused by context size fitter. When receiving such events you just need to resize the parent RectTransform accordingly like in the example below.

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

[ExecuteInEditMode]
[RequireComponent(typeof(ContentSizeFitter))]
[RequireComponent(typeof(Text))]
public class DynamicTextBehavior : UIBehaviour
{
    public int HorizontalMargin = 0;
    public int VerticalMargin = 0;

    public bool vertical = true;
    public bool horizontal = true;

    public RectTransform ParentRectTransform { get { return (RectTransform)transform.parent; } }
    public RectTransform RectTransform { get { return (RectTransform)transform; } }

    protected override void OnRectTransformDimensionsChange()
    {
        base.OnRectTransformDimensionsChange();

        Vector3 sizeDelta = RectTransform.sizeDelta;

        if (!vertical)
            sizeDelta.y = ParentRectTransform.sizeDelta.y;
        else
            sizeDelta.y = RectTransform.sizeDelta.y + VerticalMargin * 2;

        if (!horizontal)
            sizeDelta.x = ParentRectTransform.sizeDelta.x;
        else
            sizeDelta.x = RectTransform.sizeDelta.x + HorizontalMargin * 2;

        ParentRectTransform.sizeDelta = sizeDelta;
    }
}

It’s been a while.

While I do want to keep this blogging quite casual and not stress all too much about things like consistent updates or page views the fact that it’s been two years is a bit too long.

To be honest probably the biggest thing to keep me from updating was how hard it was to display code in WordPress but it seems the new Block Editor will make things a lot smoother. Sure I could have bought myself a domain and installed WordPress there with plugins that make that easier but rather not bother with that.

public void Start()
{
    UnityEngine.Debug.Log("Hello world!");
}

The two years

A lot has happened during the past two years. At Rival Games we launched two new games Thief of Thieves : season one for PC and Xbox One and Alien: Blackout for mobile platforms. Both bigger game projects than what I had done before. I’ve also learn a great deal from my colleagues and had some really heated debates about programming. Hopefully It’ll show in my future open source projects and tutorials.

About FunUI

Decided to scrap the project after getting distracted with other stuff such as polymorphic serialization which I was considering to use with FunUI. Eventually decided to focus more on making games and tools for said games instead which I may clean up and share as separate packages if they turn out useful enough for multiple projects.