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.

Content size fitter and dynamic content – Part 1

First of all for those unfamiliar with the term dynamic content like dynamic text it’s probably best explained as content that can change in size, shape or form. Highly mutable things like player name, localized string or content fetched from the game server or database. This sort of content can be a real pain to display properly in a user interface.

For example when localizing your game, the new localized text might require a lot more space than the English counterpart. Player might want to create a ton of save files and you want to display all of them in a scroll view or you want to have inventory that the player can choose to expand. Maybe you want to fetch and display information about latest updates and events in your multiplayer game or provide the player to browse servers or running games.

A lot of these things can be achieved with efficient use of content size fitter and Auto layouts (e.g vertical layout group).

Scrollable content

A lot of games these days have so much content that it’s simply too much to display even on a huge screen running 4k resolution. Especially so if you don’t want your inventory or settings window to take the whole screen.

For cases like these it’s usually enough to use Scroll Rect which allows you to scroll the selected content both horizontally and/or vertically and a mask with a image to smoothly hide content not fit for the window.  While these are generally easy to use, the settings for the content RectTransform can be a pit tricky unless you’re familiar with anchors and pivot points

Below is an example of typical RectTransform with Scroll Rect and Mask component. The image is just the unity default Null-sprite, and the content transform in a child is assigned to the Scroll Rects content field. Vertical is disabled because vertical scrolling is not desired in this particular case.

DynamicContent_Scroll-rect

Here’s one way to setup the actual content. I’ve used top anchor with horizontal stretch for the content to fit to the Scroll Rect as well as disabled content size fitter from modifying the horizontal size of the content.

The width of the children is controlled by the vertical layout group through it’s Control Child Size: width option. If you wanted to specify the width of the children by yourself, you can simply leave that out. The Child Force Expand options are not really needed in this case because the vertical size is controlled by the attached content size fitter.

Also not the pivot point and position of the content. With y-axis pivot set to 1 and the position in y-axis set to 0 the contents position defaults to the top of the content. If it were centered it might cause weird behavior when the content is displayed especially with Scroll Rect movement set to elastic.

If also set the spacing to 5 to have some extra space between control settings and aligned the child objects to center so that if I decide to disable control child size and horizontal stretch the content will still be centered.

DynamicContent_Content

In the second part we will discuss how to use Content Size fitter to resize the parent RectTransform to implement dynamic text with a background for things like tool-tips, speech bubbles, loot names and the like.

FunUI Update : GitHub repository available

Finally got around making Git repository for the FunUI project.

Link to the GitHub project.

Some New features:

Explicit Navigation Tool

for creating explicit navigation for e.g Menu buttons or other Selectable objects in the scene. The tools can be found from QB/UI/Navigation.

PauseWindow

Simple PauseWindow that can set the time-scale to zero on open, alternatively you can tick the option off an use the Windows inherited OnOpenWindow event to assign call-back to your own pause method of choice. It also sets the Animation Update mode to unscaled time if you use tick the time-scale tick.

Window animation controller : Fade

Animator controller like Window_Scale that fades the window away using CanvasGroup when window gets closed and fades it back when it’s opened. Should also block inputs through canvas during the transition.

State-driven character controller

It’s not really difficult to understand that things as important as character controllers for a game are often subject to constant tweaks  and modifications as the development progresses and new requirements need to be met. Due to this they’re among the easiest components in your game to turn in to a mess of spaghetti code filled exceptions, conditional statements and switches.

But there’s actually a design pattern that can be used to make such components much easier to manage and maintain called the state pattern. It basically allows you to divide all the functionality to different states that handle how the character behaves to commands given by the player or artificial intelligence.

State pattern and finite-state machines are relatively well documented elsewhere in the internet so I won’t be going too deeply in to them in this tutorial. However if you want to know more about them I recommend checking out the explanation at GameDesignPatterns.com.

Download the tutorial Unity package here.

Actors and Character Controllers

Before going further it’s important to fully understand what actors and character controllers actually are and what they’re not.

Actors are usually dynamic characters that move and interact with other Actors and the game environment. They can be animals like sheep that flock among themselves, characters for player to control, friendly NPC’s or even enemy characters.

Character Controllers in the other hand are components within these actors that control how the actors move and interact with the game environment based on commands they’re given. These commands can be given to them either by the player or artificial intelligence.

Due to the broad use of characters it’s generally a good idea to keep character controllers separate from player input handling and potentially very complex artificial intelligence. This allows you to use the same character controller you use to control the actor with both the player controls and artificial intelligence. Not only that it allows the player to switch between actors or even control multiple actors at once with just few lines of code.

Creating the Character Controller

Let’s start with the actual Character Controller named CharControllerBehavior to differentiate it from the one in Unity.  This controller will be simple physics based controller with variables to control the actors movement speed, turning speed, jump power and whether it can double jump or not.

Because it’s physics based it will also need a rigid-body component for us to move it which should have with all rotations frozen to prevent it from falling over. It’ll also have simple ground sensor which simply will detect whether the actor has it’s feet on the ground or not.

But the real meat of the controller is actually in the CurrentState which contains the movement state the actor is currently in. It’s the thing that actually decides if and how the the user can interact in given ways.

CharControllerBehavior.cs


using UnityEngine;

public class CharControllerBehavior : MonoBehaviour
{
    public bool ShowDebug = false;
    public float MovementSpeed = 5.0f;
    public float JumpPower = 3.0f;
    public float TurningSpeed = 10.0f;
    public bool doubleJump = true;

    public Rigidbody RB;
    public GroundSensorBehavior GroundSensor;

    private MovementState _currentState;
    public MovementState CurrentState
    {
        get
        {
            if (_currentState == null)
                CurrentState = new MovementState();

            return _currentState;
        }
        private set { _currentState = value; }
    }

    void Start()
    {
        ChangeState(new OnGroundState());
    }

    public void ChangeState(MovementState newState)
    {
        if (newState == null)
            newState = new MovementState();

        if(ShowDebug)
            Debug.Log("<color=#0066cc>" + gameObject.name + " [Mover] : " + CurrentState.ToString() + " => " + newState.ToString() + "</color>");

        CurrentState = newState;
        CurrentState.Enter(this);
    }

    void Update()
    {
        CurrentState.Update();
    }

    void FixedUpdate()
    {
        CurrentState.FixedUpdate();
    }
}

Movement state

Below is the State which all other movement states will inherit from. It includes all methods a state needs to function. These can be expanded or overridden in inherited states.

Update and Fixed update are basically ways for the state to update itself. In This chase we need Update to process the commands as quickly as possible and Fixed Update to handle physics based movement.

Start and Exit are methods that fire when the current state in CharControllerBehavior changes. Current state will be notified by calling it’s exit method and the new state will be notified through calling it’s Enter method which in this chase also passes it the controller that it’s responsible for. Enter and Exit calls are handy if you for example want to reduce the size of the collider when user is crouching and set it back to normal during exit or use it to tell the Actors animator that the user is now jumping, stunned or dead.

DirInput and RotInput are basically inputs set through the Move and Turn methods in the state. It’s up to the state to either act upon these inputs on update or fixed update or simply ignore them (e.g actor is dead, a sleep or stunned). Even though movement is physics based I prefer to scan inputs on every frame.

MovementState.cs

using UnityEngine;

public class MovementState
{
    private CharControllerBehavior _controller;
    public CharControllerBehavior Controller
    {
        get { return _controller; }
        protected set { _controller = value; }
    }

    public Vector2 DirInput { get; protected set; }
    public float RotInput { get; protected set; }

    public virtual void Update(){}
    public virtual void FixedUpdate(){}
    public virtual void Enter(CharControllerBehavior controller){ _controller = controller; }
    public virtual void Exit(){}

    //Inputs are applied every frame but actual movement should happen in fixed update
    public virtual void Move(Vector2 direction){ DirInput = direction; }
    public virtual void Turn(float turnValue) { RotInput = turnValue; }

    public virtual void Jump(){}
    public virtual void EndJump() { }
}

On Ground State

Here’s the first actual movement state for our controller. Look how it inherits from MovementState which we described above and overrides it’s Fixed update and Jump methods.

Note how the state checks on FixedUpdate whether the actor is airborne through the controllers ground sensor component. If the player is airborne it automatically calls the controller to change to new state called a FallState.

ApplyMovement and ApplyRotation called in the FixedUpdate will handle the simple physics based movement logic based on Directional and Rotational inputs provided through the Move and Turn methods found in the base Movement State class.

If the state is given the command to jump, then the state will immediately tell the controller to change it’s state to JumpState.

Remove the Controller.transform.TransformDirection line if you want the character to move in world-space.

OnGroundState.cs

using UnityEngine;

public class OnGroundState : MovementState
{
    Vector2 prevDir = Vector2.zero; 

    public override void FixedUpdate()
    {
        if (!Controller.GroundSensor.OnGround)
            Controller.ChangeState(new FallState(false));

        ApplyMovement();
        ApplyRotation();
    }

    protected virtual void ApplyMovement()
    {
        if(DirInput.magnitude > 0.0f)
        {
            Vector3 movement = new Vector3(DirInput.x, 0.0f, DirInput.y);
            movement = movement.normalized;
            movement = Controller.transform.TransformDirection(movement);
            movement = movement * Controller.MovementSpeed;
            movement.y = Controller.RB.velocity.y;

            Controller.RB.velocity = movement;
        }
        else
        {
            //simple brake, set horizontal velocity to zero to prevent slipping.
            if (prevDir.magnitude > 0.0f)
                Controller.RB.velocity = new Vector3(0.0f, Controller.RB.velocity.y, 0.0f);
            else
                return;
        }

        prevDir = DirInput;
    }

    protected virtual void ApplyRotation()
    {
        if(RotInput != 0.0f)
            Controller.transform.Rotate(0, RotInput * Controller.TurningSpeed * Time.fixedDeltaTime, 0);
    }

    public override void Jump()
    {
        Controller.ChangeState(new JumpState(Controller.doubleJump));
    }
}

Here’s a simple sensor for detecting whether actor is grounded or not. It’s attached to a transform at the actors feet and it uses Physics.OverlapSphere to see if detects any 3d colliders except for the player.

GroundSensorBehavior.cs

using UnityEngine;

public class GroundSensorBehavior : MonoBehaviour
{
    private bool _onGround = false;
    public bool OnGround
    {
        get { return _onGround; }
        private set { _onGround = value; }
    }

    public float elevation = 0.0f;

    public LayerMask GroundLayer;
    public float Radius = 0.3f;

    void FixedUpdate()
    {
        Collider[] cols = Physics.OverlapSphere(transform.position, 0.2f, GroundLayer);

        for (int i = 0; i < cols.Length; i++)
        {
            if (cols[i].transform.root.tag == "Player")
            {
                continue;
            }
            else
            {
                OnGround = true;
                return;
            }
        }

        OnGround = false;
    }
}

Jumping State and Fall State

Here are the Jump and Fall States that where mentioned before.  The Jump state simply adds vertical velocity to the actors rigidbody for the duration of the jump.  Jump ends when EndJump is called (e.g user releases jump button) which happens automatically if it takes more than 0.3s.

These states also contain simple implementation for double jump where jump state informs the fall-state to allow jumping and where fall-state tells the next jump state to not allow it again.

JumpState.s

using UnityEngine;

public class JumpState : OnGroundState
{
    float MaxJumpTime = 0.3f;
    float JumpTimer = 0.0f;
    bool _doubleJump = false;

    public JumpState(bool doubleJump = false)
    {
        _doubleJump = doubleJump;
    }

    public override void FixedUpdate()
    {
        ApplyJump();
        ApplyRotation();
    }

    public override void Update()
    {
        JumpTimer += Time.deltaTime;
        if (JumpTimer > MaxJumpTime)
            EndJump();
    }

    public override void EndJump()
    {
        Controller.ChangeState(new FallState(_doubleJump));
    }

    void ApplyJump()
    {
        float velY = Controller.JumpPower - Controller.RB.velocity.y;
        Controller.RB.AddForce(Vector3.up * velY, ForceMode.VelocityChange);
    }
}

FallState.cs

using UnityEngine;

public class FallState : OnGroundState
{
    bool _allowJump = false;
    public FallState(bool allowJump = false)
    {
        _allowJump = allowJump;
    }

    public override void FixedUpdate()
    {
        if (Controller.GroundSensor.OnGround)
            Controller.ChangeState(new OnGroundState());
        else
        {
            ApplyRotation();
        }
    }

    public override void Jump()
    {
        if(_allowJump)
        {
            Controller.ChangeState(new JumpState());
        }
    }
}

Controlling the Actor

Now that the actor knows how to move, turn and jump based on what state it’s in it’s time to send it some commands. Below is a simple Controller behavior that takes in CharControllerBehavior which it will then control.

PlayerControllerBehavior.cs

using UnityEngine;

public class PlayerControllerBehavior : MonoBehaviour
{
    public CharControllerBehavior player;

    public void Start()
    {
        Cursor.visible = false;
    }

    public void Update()
    {
        Vector2 movement = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
        float turning = Input.GetAxis("Mouse X");

        if(player)
        {
            player.CurrentState.Move(movement);
            player.CurrentState.Turn(turning);

            if (Input.GetButtonDown("Jump"))
            {
                player.CurrentState.Jump();
            }
            else if (Input.GetButtonUp("Jump"))
            {
                player.CurrentState.EndJump();
            }
        }
    }
}

Notes

  • The physics based movement logic in this tutorial is very bare bones to keep the tutorial as simple as possible.
  • It’s not generally better to use Rigidbody.addForce with velocitychange force mode to control physics based characters because setting velocity directly interferes with external forces (e.g strong wind, knock-back from explosion etc.)
  • Many games move character through animation root-motion instead of physics.
  • State machines are also great for Artificial intelligence among many other things.

Programming: where to start?

Probably among top most confusing things in software and game development is where to actually start learning game development and programming and probably among the most damaging ideas for aspiring developer is to think that it needs to be done the hard way from the ground up by learning hardcore language like C or C++ and developing your own engine.

Sure this might work for the rare few people who posses crazy amount of discipline and self-control to actually spend around hundreds if not thousands of hours to learn how to program with bare-bones editors and CLI compilers. But most people tend to get very overwhelmed and discouraged with the slow phase and the barely non-existent visual feedback associated with this method.

Picking the language

Probably the best advice I have heard when it comes to languages is to pick one and stick with it. For beginner it’s important to understand that programming languages are not all that different as you might think and it’s way easier to learn a new language if you’re already familiar with another programming language because the fundamentals rarely change even if the syntax changes.

One good example is that if you already know C# well you basically already know how to write Java. You’ll just need to get familiar with the small differences along with new tools. Even languages like C/C++ become a lot easier to learn because you already have solid understanding on programming which makes these languages a lot less overwhelming. Learning higher level programming language like Python becomes very easy if you only need to learn how they differ from your first language.

Having good understanding about concepts such as variables, operators, loops, conditions, namespaces, object-oriented programming, libraries, frameworks and such is a lot more important than the actual syntax.

Writing code – IDE

For making the learning experience least painful as possible it’s important to get yourself a proper IDE for writing code and developing applications. Using text-editors like Notepad is very archaic for programming because they do not offer any error-highlighting or code completion (a.k.a intelisense) which really help new programmers to spot their mistakes and write working code. If you have to spend an hour to find a missing typo or a missing dot from your source code that prevents it from running you’re just wasting your time.

Personally I prefer Visual studio or visual studio code myself because they’re very approachable and the editor offers proper error highlighting and code-completion for numerous languages.

Visual feedback

Let’s face it. Many people tend to get very demotivated with programming command line interface applications when most of the software and games these days have all these wonderful graphical user interfaces. The lack of visual feedback is likely major contributor to the fact on how many people feel inspired when learning something like HTML but quickly give up when they start learning an actual programming language.

Solution: GUI Library

Many languages like Java and C# actually have very easy to use tools for creating simple form applications with text-boxes, buttons, grids, pictures and such which are fairly easy to learn even for new programmers with all the tutorials that are available online. These can be a lot more motivating to work on than simple command line apps.

Some examples

  • C# – Windows forms a.k.a WinForms
  • Java – Swing or JavaFX
  • Java (Android) – Android studio (IDE)
  • Objective-c – views

Alternative solution: Make a Game

Game development is actually a very good way to get familiar with software development. With modern game engines like Unity, Cocos, XNA and maybe unreal it’s easier than ever to get started developing simple games. Games are also good because they combine many important subjects of software developing from user interface to networking and data integration.

Developing games can also be somewhat more interesting than console applications and whatever BMI Calculators and other simple form applications.

Engines and Frameworks

  • Unity3D – C# or UnityScript (similar to TypeScript)
  • Cocos – Objective-c
  • Cocos2dx – C++ (Android & iOS)
  • Unreal – Visual scripting with nodes/blueprints and C++
  • Allegro5 – C++
  • MonoGame / XNA – C#

Start small and scale up gradually

One of the most common mistakes new developers make is that they take on more than they can chew which means the project will never get finished or even “finished” which can hit motivation really hard. Because this I recommend setting up deadlines for yourself.

When starting out you should limit the projects to take 1-3 days at most and make sure that you can finish it before the deadline. This allows you to start from scratch often and really learn from the mistakes made in first projects. By doing this you’re building a proper foundation for your projects and avoiding the need to constantly revisit old code just because you’ve learned a new pattern or just a better way to implement a feature.

From there you can slowly start doing projects that take a week, 2 weeks, month etc until you can finally start working on the software or game project of your dreams. I can guarantee that it will be better than anything you would have made as a beginner.

Workshops and Game Jams

I really recommend participating in to Game Jams and all sorts of software development workshops. They’re usually these events where groups of people gang up to develop software and games within 48 hours or less.

You’ll also meet new people there who can learn from you and vice versa. You might even stumble up on new friends or even career opportunities. The experience of developing a game or application within 48 hours in a group is also a challenge that forces you to think a bit differently and really work on your team work skills.

Save system with JSON

It’s no rocket science that for players to be able to close the application and still retain the progress they’ve made the game needs some sort of a system to save data. Such system could also be used to save custom made levels possibly made with your level editor or procedural generation as well. The sky’s the limit really.

Serialization basically means the process of saving your data to a text format which can later be deserialized and processed back to data. There are many ways to serialized data with their pro’s and cons with most common being JSON, XML, CSV and maybe binary.

Personally I prefer JSON because it’s shorter and faster to read and write and does pretty much everything you need with save data. There’s also wide array of different parsers for it so writing your own with this many options it’s generally waste of time in my opinion.

SimpleJSON parser for Unity

From parsers that work well with Unity my favorite is SimpleJSON mainly because how easy it makes the data to access and structure. To add SimpleJSON to your project you can fetch it from Unify community wiki through here or use slightly modified version with added keys properties to JSONObject and JSONNode from bottom of this post.

Using SimpleJSON

The parser is fairly straightforward to use as it’s sort of a dynamic multidimensional dictionary for storing data if that makes any sense. You can add new keys to it on the fly which can contain all sorts of stuff like integers, arrays and even objects.

Probably best way to show how it works it to actually create a new JSONObject and populate it with some actual data.


        JSONObject sObj = new JSONObject();
        sObj["Name"] = "Bob";
        sObj["Money"].AsInt = 100;

        sObj["Items"]["HealthPotion"]["ID"] = "HealthPotion";
        sObj["Items"]["HealthPotion"]["Quantity"].AsInt = 3;

        sObj["Items"]["ManaPotion"]["ID"] = "ManaPotion";
        sObj["Items"]["ManaPotion"]["Quantity"].AsInt = 3;

As you can see from the example above it’s really easy to structure and populate with new data.

Accessing the data is just as easy.


        string name = sObj["Name"];
        int money = sObj["Money"].AsInt;
        int healthPotion_quantity = sObj["Items"]["HealthPotion"]["Quantity"].AsInt;

Storing actual PlayerData

So let’s create a serialize-able class that can be easily converted to JSONObject and back. This can be done fairly easy by using explicit and implicit operators which you can find in the PlayerData.cs found below.

The “[System.Serializable]” is a handy attribute on top of a class that allows you to view and modify class and it’s properties through the editor.

PlayerData.cs


using System.Collections.Generic;
using SimpleJSON;

[System.Serializable]
public class PlayerData
{
    public string Name = "Player 1";
    public int Money = 0;
    public List<Item> Items = new List<Item>();

    public static implicit operator JSONObject(PlayerData value)
    {

        JSONObject sObj = new JSONObject();
        sObj["Name"] = value.Name;
        sObj["Money"].AsInt = value.Money;

        for (int i = 0; i < value.Items.Count; i++)
            sObj["Items"][i] = value.Items[i];

        return sObj;
    }

    public static explicit operator PlayerData(JSONNode value)
    {
        PlayerData data = new PlayerData();
        data.Name = value["Name"];
        data.Money = value["Money"].AsInt;

        for (int i = 0; i < value["Items"].Count; i++)
            data.Items.Add((Item)value["Items"][i].AsObject);

        return data;
    }
}

The PlayerData has a simple inventory as well which contains items that can also be converted to JSONObject and back. This makes the conversion more loosely coupled as it doesn’t need to know what data items contains and how they’re structured.

This is also handy because you might want to inherit item class and make a new class like weapon which would also store it’s durability or food tracking time which it takes for it to expire. It’s better to leave these exceptions to the class itself.

Item.cs

using SimpleJSON;

[System.Serializable]
public class Item
{
    public string ID = "None";
    public int Quantity = 0;

    public static implicit operator JSONObject(Item value)
    {
        JSONObject data = new JSONObject();
        data["ID"] = value.ID;
        data["Quantity"].AsInt = value.Quantity;

        return data;
    }

    public static explicit operator Item(JSONNode value)
    {
        Item data = new Item();
        data.ID = value["ID"];
        data.Quantity = value["Quantity"].AsInt;

        return data;
    }
}

With the SimpleJSON version found below you can also access the keys stored in a JSONNode like this:

        foreach (string key in sObj["Items"].Keys)
        {
            UnityEngine.Debug.Log(key);
        }

Saving and Loading the saved data

So to keep things simple I made a very basic Monobehavior that checks if there’s a save file and loads it if one exists if not then it creates a new one. StreamReader from System.IO namespace  should work on most platforms and using Application.persistentDataPath as destination for save file should work on both mobile and desktop devices.

It’s worth nothing that this way the data is stored in plaintext so if you want to store private data or prevent users from modifying the file you should encrypt it with something like AES Encryption before you save it. Alternatively if there’s no sensitive data you could also use something like MD5 to create checksum for the data to make it’s modification considerably more difficult.


using UnityEngine;
using SimpleJSON;
using System.IO;

public class SaveExample : MonoBehaviour
{
    public PlayerData PlayerData;

    void Start()
    {
        string fileName = "PlayerSave";
        
        //on windows persistentDataPath = C:\Users\User\AppData\LocalLow\*Company*\*Project*
        string path = Path.Combine(Application.persistentDataPath, fileName + ".txt");

        //Check if save file exists
        if (File.Exists(path))
        {
            Debug.Log("Save found. Loading..");

            string serializedStr = "";
            if (LoadString(path, out serializedStr))
            {
                JSONNode sObj = JSONObject.Parse(serializedStr);
                PlayerData = (PlayerData)sObj;

                Debug.Log("Loaded: " + sObj.ToString());
                return;
            }
        }
        
        Debug.Log("No save file found. Creating new..");
        PlayerData = new PlayerData() { Name = "Bob the wizzard", Money = 100 };
        PlayerData.Items.Add(new Item() { ID = "HealthPotion", Quantity = 3 });
        PlayerData.Items.Add(new Item() { ID = "ManaPotion", Quantity = 5 });

        JSONObject newData = (JSONObject)PlayerData;
        SaveString(path, newData.ToString());

        Debug.Log(PlayerData.ToString());
    }

    public bool SaveString(string FilePath, string data)
    {
        try
        {
            if (!File.Exists(FilePath))
                File.Create(FilePath).Close();

            using (StreamWriter sw = new StreamWriter(FilePath))
            {
                sw.Write(data);
                sw.Close();
            }

            return true;
        }
        catch (System.Exception e)
        {
            Debug.LogError(e);
            return false;
        }
    }

    public bool LoadString(string FilePath, out string text)
    {
        text = "";
        try
        {
            if (!File.Exists(FilePath))
                return false;

            string data = "";
            using (StreamReader sr = new StreamReader(FilePath))
            {
                data = sr.ReadToEnd();
            }

            text = data;
            return true;
        }
        catch (System.Exception e)
        {
            Debug.LogError(e);
            return false;
        }
    }
}

Tips and tricks

  • You could easily make a SaveManager or something similar and add it to the ToolBox in your game. This would make it easily accessible across the game and prevent any unnecessary re-loading.
  • Encrypting save-files or using checksum does not save your game from memory hacks.
  • Only save non-persistent data and identifiers and use flyweight pattern for the rest. For example with items it’s good to save things like it’s ID, quantity and durability because those can change and vary even between duplicate items, but stuff like items description, icon, model and such can be shared among items.
  • It’s recommended to keep data container classes fairly clean and separate from classes that handle game logic. This makes them easier to use if you need to access the data outside your game e.g in a web page or a different project.

Below is the version of SimpleJSON used in this tutorial.

SimpleJSON.cs
//#define USE_SharpZipLib
#if !UNITY_WEBPLAYER
#define USE_FileIO
#endif
/* * * * *
 * A simple JSON Parser / builder
 * ------------------------------
 *
 * It mainly has been written as a simple JSON parser. It can build a JSON string
 * from the node-tree, or generate a node tree from any valid JSON string.
 *
 * If you want to use compression when saving to file / stream / B64 you have to include
 * SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ ) in your project and
 * define "USE_SharpZipLib" at the top of the file
 *
 * Written by Bunny83
 * 2012-06-09
 *
 *
 * Features / attributes:
 * - provides strongly typed node classes and lists / dictionaries
 * - provides easy access to class members / array items / data values
 * - the parser now properly identifies types. So generating JSON with this framework should work.
 * - only double quotes (") are used for quoting strings.
 * - provides "casting" properties to easily convert to / from those types:
 *   int / float / double / bool
 * - provides a common interface for each node so no explicit casting is required.
 * - the parser tries to avoid errors, but if malformed JSON is parsed the result is more or less undefined
 * - It can serialize/deserialize a node tree into/from an experimental compact binary format. It might
 *   be handy if you want to store things in a file and don't want it to be easily modifiable
 *
 *
 * 2012-12-17 Update:
 * - Added internal JSONLazyCreator class which simplifies the construction of a JSON tree
 *   Now you can simple reference any item that doesn't exist yet and it will return a JSONLazyCreator
 *   The class determines the required type by it's further use, creates the type and removes itself.
 * - Added binary serialization / deserialization.
 * - Added support for BZip2 zipped binary format. Requires the SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ )
 *   The usage of the SharpZipLib library can be disabled by removing or commenting out the USE_SharpZipLib define at the top
 * - The serializer uses different types when it comes to store the values. Since my data values
 *   are all of type string, the serializer will "try" which format fits best. The order is: int, float, double, bool, string.
 *   It's not the most efficient way but for a moderate amount of data it should work on all platforms.
 *
 * 2017-03-08 Update:
 * - Optimised parsing by using a StringBuilder for token. This prevents performance issues when large
 *   string data fields are contained in the json data.
 * - Finally refactored the badly named JSONClass into JSONObject.
 * - Replaced the old JSONData class by distict typed classes ( JSONString, JSONNumber, JSONBool, JSONNull ) this
 *   allows to propertly convert the node tree back to json without type information loss. The actual value
 *   parsing now happens at parsing time and not when you actually access one of the casting properties.
 *
 * 2017-04-11 Update:
 * - Fixed parsing bug where empty string values have been ignored.
 * - Optimised "ToString" by using a StringBuilder internally. This should heavily improve performance for large files
 * - Changed the overload of "ToString(string aIndent)" to "ToString(int aIndent)"
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2012-2017 Markus Göbel
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * * * * */
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SimpleJSON
{
    public enum JSONNodeType
    {
        Array = 1,
        Object = 2,
        String = 3,
        Number = 4,
        NullValue = 5,
        Boolean = 6,
        None = 7,
    }
    public enum JSONTextMode
    {
        Compact,
        Indent
    }

    public abstract partial class JSONNode
    {
        #region common interface

        public virtual JSONNode this[int aIndex] { get { return null; } set { } }

        public virtual JSONNode this[string aKey] { get { return null; } set { } }

        public virtual string Value { get { return ""; } set { } }

        public virtual int Count { get { return 0; } }

        public virtual bool IsNumber { get { return false; } }
        public virtual bool IsString { get { return false; } }
        public virtual bool IsBoolean { get { return false; } }
        public virtual bool IsNull { get { return false; } }
        public virtual bool IsArray { get { return false; } }
        public virtual bool IsObject { get { return false; } }

        public virtual void Add(string aKey, JSONNode aItem)
        {
        }
        public virtual void Add(JSONNode aItem)
        {
            Add("", aItem);
        }

        public virtual IEnumerable<string> Keys { get { yield break; } }

        public virtual JSONNode Remove(string aKey)
        {
            return null;
        }

        public virtual JSONNode Remove(int aIndex)
        {
            return null;
        }

        public virtual JSONNode Remove(JSONNode aNode)
        {
            return aNode;
        }

        public virtual IEnumerable<JSONNode> Children
        {
            get
            {
                yield break;
            }
        }

        public IEnumerable<JSONNode> DeepChildren
        {
            get
            {
                foreach (var C in Children)
                    foreach (var D in C.DeepChildren)
                        yield return D;
            }
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            WriteToStringBuilder(sb, 0, 0, JSONTextMode.Compact);
            return sb.ToString();
        }

        public virtual string ToString(int aIndent)
        {
            StringBuilder sb = new StringBuilder();
            WriteToStringBuilder(sb, 0, aIndent, JSONTextMode.Indent);
            return sb.ToString();
        }
        internal abstract void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode);

        #endregion common interface

        #region typecasting properties

        public abstract JSONNodeType Tag { get; }

        public virtual double AsDouble
        {
            get
            {
                double v = 0.0;
                if (double.TryParse(Value, out v))
                    return v;
                return 0.0;
            }
            set
            {
                Value = value.ToString();
            }
        }

        public virtual int AsInt
        {
            get { return (int)AsDouble; }
            set { AsDouble = value; }
        }

        public virtual float AsFloat
        {
            get { return (float)AsDouble; }
            set { AsDouble = value; }
        }

        public virtual bool AsBool
        {
            get
            {
                bool v = false;
                if (bool.TryParse(Value, out v))
                    return v;
                return !string.IsNullOrEmpty(Value);
            }
            set
            {
                Value = (value) ? "true" : "false";
            }
        }

        public virtual JSONArray AsArray
        {
            get
            {
                return this as JSONArray;
            }
        }

        public virtual JSONObject AsObject
        {
            get
            {
                return this as JSONObject;
            }
        }

        #endregion typecasting properties

        #region operators

        public static implicit operator JSONNode(string s)
        {
            return new JSONString(s);
        }
        public static implicit operator string(JSONNode d)
        {
            return (d == null) ? null : d.Value;
        }

        public static implicit operator JSONNode(double n)
        {
            return new JSONNumber(n);
        }
        public static implicit operator double(JSONNode d)
        {
            return (d == null) ? 0 : d.AsDouble;
        }

        public static implicit operator JSONNode(float n)
        {
            return new JSONNumber(n);
        }
        public static implicit operator float(JSONNode d)
        {
            return (d == null) ? 0 : d.AsFloat;
        }

        public static implicit operator JSONNode(int n)
        {
            return new JSONNumber(n);
        }
        public static implicit operator int(JSONNode d)
        {
            return (d == null) ? 0 : d.AsInt;
        }

        public static implicit operator JSONNode(bool b)
        {
            return new JSONBool(b);
        }
        public static implicit operator bool(JSONNode d)
        {
            return (d == null) ? false : d.AsBool;
        }

        public static bool operator ==(JSONNode a, object b)
        {
            if (ReferenceEquals(a, b))
                return true;
            bool aIsNull = a is JSONNull || ReferenceEquals(a, null) || a is JSONLazyCreator;
            bool bIsNull = b is JSONNull || ReferenceEquals(b, null) || b is JSONLazyCreator;
            if (aIsNull && bIsNull)
                return true;
            return a.Equals(b);
        }

        public static bool operator !=(JSONNode a, object b)
        {
            return !(a == b);
        }

        public override bool Equals(object obj)
        {
            return ReferenceEquals(this, obj);
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        #endregion operators
        internal static StringBuilder m_EscapeBuilder = new StringBuilder();
        internal static string Escape(string aText)
        {
            m_EscapeBuilder.Length = 0;
            if (m_EscapeBuilder.Capacity < aText.Length + aText.Length / 10)
                m_EscapeBuilder.Capacity = aText.Length + aText.Length / 10;
            foreach (char c in aText)
            {
                switch (c)
                {
                    case '\\':
                        m_EscapeBuilder.Append("\\\\");
                        break;
                    case '\"':
                        m_EscapeBuilder.Append("\\\"");
                        break;
                    case '\n':
                        m_EscapeBuilder.Append("\\n");
                        break;
                    case '\r':
                        m_EscapeBuilder.Append("\\r");
                        break;
                    case '\t':
                        m_EscapeBuilder.Append("\\t");
                        break;
                    case '\b':
                        m_EscapeBuilder.Append("\\b");
                        break;
                    case '\f':
                        m_EscapeBuilder.Append("\\f");
                        break;
                    default:
                        m_EscapeBuilder.Append(c);
                        break;
                }
            }
            string result = m_EscapeBuilder.ToString();
            m_EscapeBuilder.Length = 0;
            return result;
        }

        static void ParseElement(JSONNode ctx, string token, string tokenName, bool quoted)
        {
            if (quoted)
            {
                ctx.Add(tokenName, token);
                return;
            }
            string tmp = token.ToLower();
            if (tmp == "false" || tmp == "true")
                ctx.Add(tokenName, tmp == "true");
            else if (tmp == "null")
                ctx.Add(tokenName, null);
            else
            {
                double val;
                if (double.TryParse(token, out val))
                    ctx.Add(tokenName, val);
                else
                    ctx.Add(tokenName, token);
            }
        }

        public static JSONNode Parse(string aJSON)
        {
            Stack<JSONNode> stack = new Stack<JSONNode>();
            JSONNode ctx = null;
            int i = 0;
            StringBuilder Token = new StringBuilder();
            string TokenName = "";
            bool QuoteMode = false;
            bool TokenIsQuoted = false;
            while (i < aJSON.Length)             {                 switch (aJSON[i])                 {                     case '{':                         if (QuoteMode)                         {                             Token.Append(aJSON[i]);                             break;                         }                         stack.Push(new JSONObject());                         if (ctx != null)                         {                             ctx.Add(TokenName, stack.Peek());                         }                         TokenName = "";                         Token.Length = 0;                         ctx = stack.Peek();                         break;                     case '[':                         if (QuoteMode)                         {                             Token.Append(aJSON[i]);                             break;                         }                         stack.Push(new JSONArray());                         if (ctx != null)                         {                             ctx.Add(TokenName, stack.Peek());                         }                         TokenName = "";                         Token.Length = 0;                         ctx = stack.Peek();                         break;                     case '}':                     case ']':                         if (QuoteMode)                         {                             Token.Append(aJSON[i]);                             break;                         }                         if (stack.Count == 0)                             throw new Exception("JSON Parse: Too many closing brackets");                         stack.Pop();                         if (Token.Length > 0 || TokenIsQuoted)
                        {
                            ParseElement(ctx, Token.ToString(), TokenName, TokenIsQuoted);
                            TokenIsQuoted = false;
                        }
                        TokenName = "";
                        Token.Length = 0;
                        if (stack.Count > 0)
                            ctx = stack.Peek();
                        break;

                    case ':':
                        if (QuoteMode)
                        {
                            Token.Append(aJSON[i]);
                            break;
                        }
                        TokenName = Token.ToString();
                        Token.Length = 0;
                        TokenIsQuoted = false;
                        break;

                    case '"':
                        QuoteMode ^= true;
                        TokenIsQuoted |= QuoteMode;
                        break;

                    case ',':
                        if (QuoteMode)
                        {
                            Token.Append(aJSON[i]);
                            break;
                        }
                        if (Token.Length > 0 || TokenIsQuoted)
                        {
                            ParseElement(ctx, Token.ToString(), TokenName, TokenIsQuoted);
                            TokenIsQuoted = false;
                        }
                        TokenName = "";
                        Token.Length = 0;
                        TokenIsQuoted = false;
                        break;

                    case '\r':
                    case '\n':
                        break;

                    case ' ':
                    case '\t':
                        if (QuoteMode)
                            Token.Append(aJSON[i]);
                        break;

                    case '\\':
                        ++i;
                        if (QuoteMode)
                        {
                            char C = aJSON[i];
                            switch (C)
                            {
                                case 't':
                                    Token.Append('\t');
                                    break;
                                case 'r':
                                    Token.Append('\r');
                                    break;
                                case 'n':
                                    Token.Append('\n');
                                    break;
                                case 'b':
                                    Token.Append('\b');
                                    break;
                                case 'f':
                                    Token.Append('\f');
                                    break;
                                case 'u':
                                    {
                                        string s = aJSON.Substring(i + 1, 4);
                                        Token.Append((char)int.Parse(
                                            s,
                                            System.Globalization.NumberStyles.AllowHexSpecifier));
                                        i += 4;
                                        break;
                                    }
                                default:
                                    Token.Append(C);
                                    break;
                            }
                        }
                        break;

                    default:
                        Token.Append(aJSON[i]);
                        break;
                }
                ++i;
            }
            if (QuoteMode)
            {
                throw new Exception("JSON Parse: Quotation marks seems to be messed up.");
            }
            return ctx;
        }

        public virtual void Serialize(System.IO.BinaryWriter aWriter)
        {
        }

        public void SaveToStream(System.IO.Stream aData)
        {
            var W = new System.IO.BinaryWriter(aData);
            Serialize(W);
        }

#if USE_SharpZipLib
		public void SaveToCompressedStream(System.IO.Stream aData)
		{
			using (var gzipOut = new ICSharpCode.SharpZipLib.BZip2.BZip2OutputStream(aData))
			{
				gzipOut.IsStreamOwner = false;
				SaveToStream(gzipOut);
				gzipOut.Close();
			}
		}

		public void SaveToCompressedFile(string aFileName)
		{

#if USE_FileIO
			System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName);
			using(var F = System.IO.File.OpenWrite(aFileName))
			{
				SaveToCompressedStream(F);
			}

#else
			throw new Exception("Can't use File IO stuff in the webplayer");
#endif
		}
		public string SaveToCompressedBase64()
		{
			using (var stream = new System.IO.MemoryStream())
			{
				SaveToCompressedStream(stream);
				stream.Position = 0;
				return System.Convert.ToBase64String(stream.ToArray());
			}
		}

#else
        public void SaveToCompressedStream(System.IO.Stream aData)
        {
            throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
        }

        public void SaveToCompressedFile(string aFileName)
        {
            throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
        }

        public string SaveToCompressedBase64()
        {
            throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
        }
#endif

        public void SaveToFile(string aFileName)
        {
#if USE_FileIO
            System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName);
            using (var F = System.IO.File.OpenWrite(aFileName))
            {
                SaveToStream(F);
            }
#else
			throw new Exception ("Can't use File IO stuff in the webplayer");
#endif
        }

        public string SaveToBase64()
        {
            using (var stream = new System.IO.MemoryStream())
            {
                SaveToStream(stream);
                stream.Position = 0;
                return System.Convert.ToBase64String(stream.ToArray());
            }
        }

        public static JSONNode Deserialize(System.IO.BinaryReader aReader)
        {
            JSONNodeType type = (JSONNodeType)aReader.ReadByte();
            switch (type)
            {
                case JSONNodeType.Array:
                    {
                        int count = aReader.ReadInt32();
                        JSONArray tmp = new JSONArray();
                        for (int i = 0; i < count; i++)
                            tmp.Add(Deserialize(aReader));
                        return tmp;
                    }
                case JSONNodeType.Object:
                    {
                        int count = aReader.ReadInt32();
                        JSONObject tmp = new JSONObject();
                        for (int i = 0; i < count; i++)
                        {
                            string key = aReader.ReadString();
                            var val = Deserialize(aReader);
                            tmp.Add(key, val);
                        }
                        return tmp;
                    }
                case JSONNodeType.String:
                    {
                        return new JSONString(aReader.ReadString());
                    }
                case JSONNodeType.Number:
                    {
                        return new JSONNumber(aReader.ReadDouble());
                    }
                case JSONNodeType.Boolean:
                    {
                        return new JSONBool(aReader.ReadBoolean());
                    }
                case JSONNodeType.NullValue:
                    {
                        return new JSONNull();
                    }
                default:
                    {
                        throw new Exception("Error deserializing JSON. Unknown tag: " + type);
                    }
            }
        }

#if USE_SharpZipLib
		public static JSONNode LoadFromCompressedStream(System.IO.Stream aData)
		{
			var zin = new ICSharpCode.SharpZipLib.BZip2.BZip2InputStream(aData);
			return LoadFromStream(zin);
		}
		public static JSONNode LoadFromCompressedFile(string aFileName)
		{
#if USE_FileIO
			using(var F = System.IO.File.OpenRead(aFileName))
			{
				return LoadFromCompressedStream(F);
			}
#else
			throw new Exception("Can't use File IO stuff in the webplayer");
#endif
		}
		public static JSONNode LoadFromCompressedBase64(string aBase64)
		{
			var tmp = System.Convert.FromBase64String(aBase64);
			var stream = new System.IO.MemoryStream(tmp);
			stream.Position = 0;
			return LoadFromCompressedStream(stream);
		}
#else
        public static JSONNode LoadFromCompressedFile(string aFileName)
        {
            throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
        }

        public static JSONNode LoadFromCompressedStream(System.IO.Stream aData)
        {
            throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
        }

        public static JSONNode LoadFromCompressedBase64(string aBase64)
        {
            throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON");
        }
#endif

        public static JSONNode LoadFromStream(System.IO.Stream aData)
        {
            using (var R = new System.IO.BinaryReader(aData))
            {
                return Deserialize(R);
            }
        }

        public static JSONNode LoadFromFile(string aFileName)
        {
#if USE_FileIO
            using (var F = System.IO.File.OpenRead(aFileName))
            {
                return LoadFromStream(F);
            }
#else
			throw new Exception ("Can't use File IO stuff in the webplayer");
#endif
        }

        public static JSONNode LoadFromBase64(string aBase64)
        {
            var tmp = System.Convert.FromBase64String(aBase64);
            var stream = new System.IO.MemoryStream(tmp);
            stream.Position = 0;
            return LoadFromStream(stream);
        }
    }
    // End of JSONNode

    public class JSONArray : JSONNode, IEnumerable
    {
        private List<JSONNode> m_List = new List<JSONNode>();
        public bool inline = false;

        public override JSONNodeType Tag { get { return JSONNodeType.Array; } }
        public override bool IsArray { get { return true; } }

        public override JSONNode this[int aIndex]
        {
            get
            {
                if (aIndex < 0 || aIndex >= m_List.Count)
                    return new JSONLazyCreator(this);
                return m_List[aIndex];
            }
            set
            {
                if (value == null)
                    value = new JSONNull();
                if (aIndex < 0 || aIndex >= m_List.Count)
                    m_List.Add(value);
                else
                    m_List[aIndex] = value;
            }
        }

        public override JSONNode this[string aKey]
        {
            get { return new JSONLazyCreator(this); }
            set
            {
                if (value == null)
                    value = new JSONNull();
                m_List.Add(value);
            }
        }

        public override int Count
        {
            get { return m_List.Count; }
        }

        public override void Add(string aKey, JSONNode aItem)
        {
            if (aItem == null)
                aItem = new JSONNull();
            m_List.Add(aItem);
        }

        public override JSONNode Remove(int aIndex)
        {
            if (aIndex < 0 || aIndex >= m_List.Count)
                return null;
            JSONNode tmp = m_List[aIndex];
            m_List.RemoveAt(aIndex);
            return tmp;
        }

        public override JSONNode Remove(JSONNode aNode)
        {
            m_List.Remove(aNode);
            return aNode;
        }

        public override IEnumerable<JSONNode> Children
        {
            get
            {
                foreach (JSONNode N in m_List)
                    yield return N;
            }
        }

        public IEnumerator GetEnumerator()
        {
            foreach (JSONNode N in m_List)
                yield return N;
        }

        public override void Serialize(System.IO.BinaryWriter aWriter)
        {
            aWriter.Write((byte)JSONNodeType.Array);
            aWriter.Write(m_List.Count);
            for (int i = 0; i < m_List.Count; i++)
            {
                m_List[i].Serialize(aWriter);
            }
        }

        internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
        {
            aSB.Append('[');
            int count = m_List.Count;
            if (inline)
                aMode = JSONTextMode.Compact;
            for (int i = 0; i < count; i++)             {                 if (i > 0)
                    aSB.Append(',');
                if (aMode == JSONTextMode.Indent)
                    aSB.AppendLine();

                if (aMode == JSONTextMode.Indent)
                    aSB.Append(' ', aIndent + aIndentInc);
                m_List[i].WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode);
            }
            if (aMode == JSONTextMode.Indent)
                aSB.AppendLine().Append(' ', aIndent);
            aSB.Append(']');
        }
    }
    // End of JSONArray

    public class JSONObject : JSONNode, IEnumerable
    {
        private Dictionary<string, JSONNode> m_Dict = new Dictionary<string, JSONNode>();

        public bool inline = false;

        public override JSONNodeType Tag { get { return JSONNodeType.Object; } }
        public override bool IsObject { get { return true; } }

        public override IEnumerable<string> Keys
        {
            get
            {
                foreach (var key in m_Dict.Keys)
                    yield return key;
            }
        }

        public override JSONNode this[string aKey]
        {
            get
            {
                if (m_Dict.ContainsKey(aKey))
                    return m_Dict[aKey];
                else
                    return new JSONLazyCreator(this, aKey);
            }
            set
            {
                if (value == null)
                    value = new JSONNull();
                if (m_Dict.ContainsKey(aKey))
                    m_Dict[aKey] = value;
                else
                    m_Dict.Add(aKey, value);
            }
        }

        public override JSONNode this[int aIndex]
        {
            get
            {
                if (aIndex < 0 || aIndex >= m_Dict.Count)
                    return null;
                return m_Dict.ElementAt(aIndex).Value;
            }
            set
            {
                if (value == null)
                    value = new JSONNull();
                if (aIndex < 0 || aIndex >= m_Dict.Count)
                    return;
                string key = m_Dict.ElementAt(aIndex).Key;
                m_Dict[key] = value;
            }
        }

        public override int Count
        {
            get { return m_Dict.Count; }
        }

        public override void Add(string aKey, JSONNode aItem)
        {
            if (aItem == null)
                aItem = new JSONNull();

            if (!string.IsNullOrEmpty(aKey))
            {
                if (m_Dict.ContainsKey(aKey))
                    m_Dict[aKey] = aItem;
                else
                    m_Dict.Add(aKey, aItem);
            }
            else
                m_Dict.Add(Guid.NewGuid().ToString(), aItem);
        }

        public override JSONNode Remove(string aKey)
        {
            if (!m_Dict.ContainsKey(aKey))
                return null;
            JSONNode tmp = m_Dict[aKey];
            m_Dict.Remove(aKey);
            return tmp;
        }

        public override JSONNode Remove(int aIndex)
        {
            if (aIndex < 0 || aIndex >= m_Dict.Count)
                return null;
            var item = m_Dict.ElementAt(aIndex);
            m_Dict.Remove(item.Key);
            return item.Value;
        }

        public override JSONNode Remove(JSONNode aNode)
        {
            try
            {
                var item = m_Dict.Where(k => k.Value == aNode).First();
                m_Dict.Remove(item.Key);
                return aNode;
            }
            catch
            {
                return null;
            }
        }

        public override IEnumerable<JSONNode> Children
        {
            get
            {
                foreach (KeyValuePair<string, JSONNode> N in m_Dict)
                    yield return N.Value;
            }
        }

        public IEnumerator GetEnumerator()
        {
            foreach (KeyValuePair<string, JSONNode> N in m_Dict)
                yield return N;
        }

        public override void Serialize(System.IO.BinaryWriter aWriter)
        {
            aWriter.Write((byte)JSONNodeType.Object);
            aWriter.Write(m_Dict.Count);
            foreach (string K in m_Dict.Keys)
            {
                aWriter.Write(K);
                m_Dict[K].Serialize(aWriter);
            }
        }
        internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
        {
            aSB.Append('{');
            bool first = true;
            if (inline)
                aMode = JSONTextMode.Compact;
            foreach (var k in m_Dict)
            {
                if (!first)
                    aSB.Append(',');
                first = false;
                if (aMode == JSONTextMode.Indent)
                    aSB.AppendLine();
                if (aMode == JSONTextMode.Indent)
                    aSB.Append(' ', aIndent + aIndentInc);
                aSB.Append('\"').Append(Escape(k.Key)).Append('\"');
                if (aMode == JSONTextMode.Compact)
                    aSB.Append(':');
                else
                    aSB.Append(" : ");
                k.Value.WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode);
            }
            if (aMode == JSONTextMode.Indent)
                aSB.AppendLine().Append(' ', aIndent);
            aSB.Append('}');
        }

    }
    // End of JSONObject

    public class JSONString : JSONNode
    {
        private string m_Data;

        public override JSONNodeType Tag { get { return JSONNodeType.String; } }
        public override bool IsString { get { return true; } }

        public override string Value
        {
            get { return m_Data; }
            set
            {
                m_Data = value;
            }
        }

        public JSONString(string aData)
        {
            m_Data = aData;
        }

        public override void Serialize(System.IO.BinaryWriter aWriter)
        {
            aWriter.Write((byte)JSONNodeType.String);
            aWriter.Write(m_Data);
        }
        internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
        {
            aSB.Append('\"').Append(Escape(m_Data)).Append('\"');
        }
        public override bool Equals(object obj)
        {
            if (base.Equals(obj))
                return true;
            string s = obj as string;
            if (s != null)
                return m_Data == s;
            JSONString s2 = obj as JSONString;
            if (s2 != null)
                return m_Data == s2.m_Data;
            return false;
        }
        public override int GetHashCode()
        {
            return m_Data.GetHashCode();
        }
    }
    // End of JSONString

    public class JSONNumber : JSONNode
    {
        private double m_Data;

        public override JSONNodeType Tag { get { return JSONNodeType.Number; } }
        public override bool IsNumber { get { return true; } }

        public override string Value
        {
            get { return m_Data.ToString(); }
            set
            {
                double v;
                if (double.TryParse(value, out v))
                    m_Data = v;
            }
        }

        public override double AsDouble
        {
            get { return m_Data; }
            set { m_Data = value; }
        }

        public JSONNumber(double aData)
        {
            m_Data = aData;
        }

        public JSONNumber(string aData)
        {
            Value = aData;
        }

        public override void Serialize(System.IO.BinaryWriter aWriter)
        {
            aWriter.Write((byte)JSONNodeType.Number);
            aWriter.Write(m_Data);
        }
        internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
        {
            aSB.Append(m_Data);
        }
        private static bool IsNumeric(object value)
        {
            return value is int || value is uint
                || value is float || value is double
                || value is decimal
                || value is long || value is ulong
                || value is short || value is ushort
                || value is sbyte || value is byte;
        }
        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;
            if (base.Equals(obj))
                return true;
            JSONNumber s2 = obj as JSONNumber;
            if (s2 != null)
                return m_Data == s2.m_Data;
            if (IsNumeric(obj))
                return Convert.ToDouble(obj) == m_Data;
            return false;
        }
        public override int GetHashCode()
        {
            return m_Data.GetHashCode();
        }
    }
    // End of JSONNumber

    public class JSONBool : JSONNode
    {
        private bool m_Data;

        public override JSONNodeType Tag { get { return JSONNodeType.Boolean; } }
        public override bool IsBoolean { get { return true; } }

        public override string Value
        {
            get { return m_Data.ToString(); }
            set
            {
                bool v;
                if (bool.TryParse(value, out v))
                    m_Data = v;
            }
        }
        public override bool AsBool
        {
            get { return m_Data; }
            set { m_Data = value; }
        }

        public JSONBool(bool aData)
        {
            m_Data = aData;
        }

        public JSONBool(string aData)
        {
            Value = aData;
        }

        public override void Serialize(System.IO.BinaryWriter aWriter)
        {
            aWriter.Write((byte)JSONNodeType.Boolean);
            aWriter.Write(m_Data);
        }
        internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
        {
            aSB.Append((m_Data) ? "true" : "false");
        }
        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;
            if (obj is bool)
                return m_Data == (bool)obj;
            return false;
        }
        public override int GetHashCode()
        {
            return m_Data.GetHashCode();
        }
    }
    // End of JSONBool

    public class JSONNull : JSONNode
    {

        public override JSONNodeType Tag { get { return JSONNodeType.NullValue; } }
        public override bool IsNull { get { return true; } }

        public override string Value
        {
            get { return "null"; }
            set { }
        }
        public override bool AsBool
        {
            get { return false; }
            set { }
        }

        public override bool Equals(object obj)
        {
            if (object.ReferenceEquals(this, obj))
                return true;
            return (obj is JSONNull);
        }
        public override int GetHashCode()
        {
            return 0;
        }

        public override void Serialize(System.IO.BinaryWriter aWriter)
        {
            aWriter.Write((byte)JSONNodeType.NullValue);
        }
        internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
        {
            aSB.Append("null");
        }
    }
    // End of JSONNull

    internal class JSONLazyCreator : JSONNode
    {
        private JSONNode m_Node = null;
        private string m_Key = null;

        public override JSONNodeType Tag { get { return JSONNodeType.None; } }

        public JSONLazyCreator(JSONNode aNode)
        {
            m_Node = aNode;
            m_Key = null;
        }

        public JSONLazyCreator(JSONNode aNode, string aKey)
        {
            m_Node = aNode;
            m_Key = aKey;
        }

        private void Set(JSONNode aVal)
        {
            if (m_Key == null)
            {
                m_Node.Add(aVal);
            }
            else
            {
                m_Node.Add(m_Key, aVal);
            }
            m_Node = null; // Be GC friendly.
        }

        public override JSONNode this[int aIndex]
        {
            get
            {
                return new JSONLazyCreator(this);
            }
            set
            {
                var tmp = new JSONArray();
                tmp.Add(value);
                Set(tmp);
            }
        }

        public override JSONNode this[string aKey]
        {
            get
            {
                return new JSONLazyCreator(this, aKey);
            }
            set
            {
                var tmp = new JSONObject();
                tmp.Add(aKey, value);
                Set(tmp);
            }
        }

        public override void Add(JSONNode aItem)
        {
            var tmp = new JSONArray();
            tmp.Add(aItem);
            Set(tmp);
        }

        public override void Add(string aKey, JSONNode aItem)
        {
            var tmp = new JSONObject();
            tmp.Add(aKey, aItem);
            Set(tmp);
        }

        public static bool operator ==(JSONLazyCreator a, object b)
        {
            if (b == null)
                return true;
            return System.Object.ReferenceEquals(a, b);
        }

        public static bool operator !=(JSONLazyCreator a, object b)
        {
            return !(a == b);
        }

        public override bool Equals(object obj)
        {
            if (obj == null)
                return true;
            return System.Object.ReferenceEquals(this, obj);
        }

        public override int GetHashCode()
        {
            return 0;
        }

        public override int AsInt
        {
            get
            {
                JSONNumber tmp = new JSONNumber(0);
                Set(tmp);
                return 0;
            }
            set
            {
                JSONNumber tmp = new JSONNumber(value);
                Set(tmp);
            }
        }

        public override float AsFloat
        {
            get
            {
                JSONNumber tmp = new JSONNumber(0.0f);
                Set(tmp);
                return 0.0f;
            }
            set
            {
                JSONNumber tmp = new JSONNumber(value);
                Set(tmp);
            }
        }

        public override double AsDouble
        {
            get
            {
                JSONNumber tmp = new JSONNumber(0.0);
                Set(tmp);
                return 0.0;
            }
            set
            {
                JSONNumber tmp = new JSONNumber(value);
                Set(tmp);
            }
        }

        public override bool AsBool
        {
            get
            {
                JSONBool tmp = new JSONBool(false);
                Set(tmp);
                return false;
            }
            set
            {
                JSONBool tmp = new JSONBool(value);
                Set(tmp);
            }
        }

        public override JSONArray AsArray
        {
            get
            {
                JSONArray tmp = new JSONArray();
                Set(tmp);
                return tmp;
            }
        }

        public override JSONObject AsObject
        {
            get
            {
                JSONObject tmp = new JSONObject();
                Set(tmp);
                return tmp;
            }
        }
        internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
        {
            aSB.Append("null");
        }
    }
    // End of JSONLazyCreator

    public static class JSON
    {
        public static JSONNode Parse(string aJSON)
        {
            return JSONNode.Parse(aJSON);
        }
    }
}