Tutorial: Saving script values in Unity with Reflection

For our project DawnRoot, We wanted to have an interactive AI, Not AI standing in the same spot giving the same lines of dialog every time. Instead, we wanted our AI to move around the world, meet their needs for food, drink, and sleep, and otherwise appear as life-like as possible. To that end, we implemented a system where each action (e.g. eat, drink, sleep, etc.) was its own script. This works quite well as an organizational method, as the actions an AI can perform are limited simply by the actions that they have attached to their game object. However, what we ended up in is a situation in which all actions are derived from a single parent class, but nevertheless have their own unique variables to keep track of. This was fine until it came time to save the AI’s state. Our options were to have either a specific saving function / struct for each AI action, as well as an entry for that struct in the AI’s save file, or to find a way to make a single save method work for all of the AI actions.

It turns out that it is a relatively simple matter to create a general purpose struct for holding script save data, using a feature of C# called reflection.

What is reflection? Put simply, it is the ability for a program to “Reflect” on its own state and make decisions based upon what it finds. For example, Reflection lets you search for variables in a class by name, or get the type of an unknown object. This flexibility comes at a cost however, and it should be noted that reflection is very slow. In our case using it for a function that only gets called occasionally isn’t so bad, but if reflection is needed every frame then your performance is bound to suffer.

With that in mind, let us begin! The goal of this tutorial is to use reflection to create a struct capable of storing the state of any script. While reflection works on any variable, I’m going to restrict the tutorial to covering serializable variables, so that the struct can be saved to disk without any hicups. To test our save method, we’re going to use the following two scripts. Notice that the scripts have no common variables with the exception of the dataHolder, which will hold the save data structs that we create.

screen_shot_2016-09-16_at_11-39-59_am

screen_shot_2016-09-16_at_11-40-16_am

To start, let’s create our files. For the purpose of this tutorial, I’m going to create four files: SaveDataHolder.cs, ScriptSaveData.cs, TestScript1.cs, and TestScript2.cs. ScriptSaveData.cs will be our main script, but the other three are needed to demonstrate how the save system works.

screen_shot_2016-09-16_at_11-48-25_am

Open ScriptSaveData.cs. We will start by adding a couple ‘using’ statements to the top of our file. Add using statements for System, System.Collections.Generic, and System.Reflection. Once done, your file should look like this:

using UnityEngine;
using System.Collections;
using System;
using System.Collections.Generic;
using System.Reflection;

public class ScriptSaveData : MonoBehaviour {

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
	
	}
}

The next step is to change our class into a struct to save our information into.

public struct ScriptSaveData {

	public ScriptSaveData() {
	
	}
}

To save the state of any script, we will need at least two variables for our save data struct. The first is the Type of the action, since we want to be able to store one of any number possible scripts. The second is a collection of all of the variables in the subclass. In this case, I’m using a <string, object> dictionary to store the name of the variable and its associated value.

 

public struct ScriptSaveData {

	public Type scriptType;
	public Dictionary<string, object> scriptVariables;
	
	public ScriptSaveData() {
	
	}
}

Now we are ready to start making the information gathering function. In this case, I’m going to use the constructor function of our save data struct for that purpose. This way, we can pass the constructor an object, and have the save data for that object returned to us. Start by setting up the constructor function to initialize our variables.

public ScriptSaveData(object script) {
    scriptType;
    scriptVariables = new Dictionary<string, object>();
}

It is now time to use our first bit of reflection! To get the type of this action, call the reflection function GetType() after the passed in action like so.

public ScriptSaveData(object script) {
	scriptType = script.GetType();
	scriptVariables = new Dictionary<string, object>();
}

Next up is the meat of the data gathering, using reflection to make a list of all of the variables in the subclass. To do this, we’ll use the GetFields() function.

In C#, fields refer to variables without any accessors, e.g.

public float x = 3.3f;

Properties refer to those variables with accessors, e.g.

public float X {
	get { return x; }
}

The GetFields function can take a number of binary flags to help filter what values to acquire. For our purposes, we want to use the Instance flag, and both the Public and nonPublic flags. The instance flag indicates that we want the fields specific to an instance, and will ignore any fields that belong to the class itself. The Public/nonPublic flags indicate that we want the private fields as well as the public ones. By connecting these flags with bitwise or operators, we can say that we would like all of the conditions during our search. This should return a list of variables for the given script, stored inside a list of FieldInfo Objects.

public ScriptSaveData(object script) {
	scriptType = script.GetType();
	scriptVariables = new Dictionary<string, object>();

	FieldInfo[] foundFields = script.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}

After making the collection of FieldInfo objects, we iterate through it to add the string and value to our collection of variables. Monobehaviours and other unity objects like Vector3s aren’t serializable, so if we store them and attempt to save our struct, we’ll get an error. Therefore we will include some extra code in this case to screen any fields that reference non-serializable objects. To do this, we use the IsSerializable property of the FieldType property. When we have a field that is of a serializable type, we simply get the name and value of it using the appropriate functions and add them to the dictionary we made earlier.

foreach(FieldInfo field in foundFields) {
	if (field.FieldType.IsSerializable) {
		scriptVariables.Add(field.Name, field.GetValue(script));
	}
}

The last step for the information gathering function is to make the struct as serializable. This will make it possible to save this struct to a file, though this tutorial won’t cover the steps for actually doing so.

[Serializable]
public struct ScriptSaveData {

	public Type scriptType;
	public Dictionary<string, object> scriptVariables;
	
	public ScriptSaveData(object script) {
		scriptType = script.GetType();
		scriptVariables = new Dictionary<string, object>();
	
		FieldInfo[] foundFields = script.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
	
		foreach(FieldInfo field in foundFields) {
			if (field.FieldType.IsSerializable) {
				scriptVariables.Add(field.Name, field.GetValue(script));
			}
		}
	}
}

Now, we can save the state of any script by simply creating a new ScriptSaveData instance!

Of course, once we put our data into a serializable format, we also want to be able to take it back out again. We are going to do this by making a new function in our ScriptSaveData struct. This function will take a script as input, and assign the values contained in the save data to the fields the target script contains.

public void LoadIntoScript(object targetScript) {

}

We need to include a check for the right type, because we want to avoid feeding it save data from the wrong script.

public void LoadIntoScript(object targetScript) {
	if (targetScript.GetType() != scriptType) {
		Debug.LogError("Tried to load the save data of a different script");
	} else {

	}
}

From here, we loop through our collection of variables, and assign them to the fields by using GetField(), SetValue(), and the same binding flags as we did to gather them.

public void LoadIntoScript(object targetScript) {
	if (targetScript.GetType() != scriptType) {
		Debug.LogError("Tried to load the save data of a different script");
	} else {
		foreach(KeyValuePair<string, object> field in scriptVariables) {
			targetScript.GetType().GetField(field.Key, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).SetValue(targetScript, field.Value);
		}
	}
}

And that’s it! the actual saving to a file is done by using C#’s serialization class, but this is covered in a large amount of tutorials, so I just wanted to focus on how you can write one save function to save all of your classes. Your final ScriptSaveData.cs file should look like this:

using UnityEngine;
using System.Collections;
using System;
using System.Collections.Generic;
using System.Reflection;

[Serializable]
public struct ScriptSaveData {

	public Type scriptType;
	public Dictionary<string, object> scriptVariables;
	
	public ScriptSaveData(object script) {
		scriptType = script.GetType();
		scriptVariables = new Dictionary<string, object>();
	
		FieldInfo[] foundFields = script.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
	
		foreach(FieldInfo field in foundFields) {
			if (field.FieldType.IsSerializable) {
				scriptVariables.Add(field.Name, field.GetValue(script));
			}
		}
	}

	public void LoadIntoScript(object targetScript) {
		if (targetScript.GetType() != scriptType) {
			Debug.LogError("Tried to load the save data of a different script");
		} else {
			foreach(KeyValuePair<string, object> field in scriptVariables) {
				targetScript.GetType().GetField(field.Key, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).SetValue(targetScript, field.Value);
			}
		}
	}
}

Now, it’s best to test it out to see if it works! First, open SaveDataHolder.cs and make it look like this:

using UnityEngine;
using System.Collections;

public class SaveDataHolder : MonoBehaviour {

	public ScriptSaveData saveData1;
	public ScriptSaveData saveData2;


}

The purpose of the savedataholder is to provide a temorary place to store the data of the two test scripts. Now, open TestScript1.cs and make it look like this:

using UnityEngine;
using System.Collections;

public class TestScript1 : MonoBehaviour {

	public SaveDataHolder dataHolder;
	
	public int power = 10;
	public float speed = 15.5f;
	public string attackName = "Fireball";
	public Vector3 spawnOffset = Vector3.zero;
	
	
	// Use this for initialization
	void Start () {
		dataHolder.saveData1 = new ScriptSaveData(this);
	}
	
	public void LoadSaveData() {
		dataHolder.saveData1.LoadIntoScript(this);
	}

}

And make TestScript2.cs look like this:

using UnityEngine;
using System.Collections;

public class TestScript2 : MonoBehaviour {

	public SaveDataHolder dataHolder;
	
	public bool active = false;
	public bool hasTarget = true;
	public string enemyName = "Ork";
	public Transform targetTransform;
	
	
	// Use this for initialization
	void Start () {
		dataHolder.saveData2 = new ScriptSaveData(this);
	}
	
	public void LoadSaveData() {
		dataHolder.saveData2.LoadIntoScript(this);
	}

}

The final step is to make a scene to hold all of our objects. Here, I have made a scene with the data holder, TestScript1, and TestScript2 attached to their own objects. I’ve also made two buttons, one each to call the ‘LoadSaveData()’ function on TestObject1 and TestObject2 respectively. Finally, I have assigned the dataHolder object to the dataholder field for both Test scripts using the inspector.

screen_shot_2016-09-16_at_12-42-46_pm

To perform our test, all we have to do is run the scene, and play with the values of the test scripts! On startup each script saves its starting values, and stores the savedata in the dataholder. When we click one of the buttons, the saved data is loaded back into the script, resetting it to its starting values.

reflectionsaving1

…well, almost. You’ll have noticed that I’ve included some unserializable fields on the test scripts. The Vector3 and the Transform are both unserializable, and so are not saved in the save data, and aren’t updated when the save data is loaded. In order to save these values, you would have to make your own custom classes/structs marked as serializable.Then, convert the unserializable value to your custom equivalent, and store the custom value as a field on the object.

Hopefully this tutorial will be usefull as you start to experiment with saving and loading your game!