Scriptable Object Variables

ScriptableObjects in Unity are super useful for all kinds of things including creating variables as assets that make it really easy to test and debug your game.

If you don’t know what scriptable objects do, check out 3 Cool Ways to Architect Your Game With Scriptable Objects at Unity’s website.

In this post I want to expand on the information in that article and show you how to create an extensible system for variables of different types and how to easily save and load them from a file.

Extending Variable Types

First of all, if you don’t want to be stuck with just one type of variable (i.e FloatVariable), you need to figure out how to create new types without constantly copy/pasting code (BAD!).

C# generics to the rescue!

public abstract class Variable<T> : ScriptableObject {
	
	public T DefaultValue;

	public T RuntimeValue { get; set; }
	
	protected void OnEnable()
	{
		RuntimeValue = DefaultValue;
	}
}

Note that the DefaultValue is persisted (can be set from the inspector), while the RuntimeValue is only available during run-time.

To add a new type of variable, just declare a new class like this:

[CreateAssetMenu(fileName = "intVar",menuName ="Variables/Integer")]
public class IntVariable : Variable<int>
{

}

Now your Variables can hold any type! You can also use your own Enums or add other functionality like Min/Max values for the IntVariable.

Tip: Add a custom icon for any scriptable object by adding a square png to Assets/Gizmos with the name ScriptableType Icon.png, like so: IntVariable Icon.png (note the space).

Variable Persistence

The need to persist the runtime values of variables between game sessions quickly becomes apparent once you start using them. If you use a scriptable object as an inventory for your RPG game, how do you save its contents with your save file?

First of all, we need to find all variables of all the types in the project. For that we will need to introduce another abstract base class that does not use generics.

public abstract class BaseVariable : ScriptableObject

The whole inheritance chain should now look like this:

IntVariable --> Variable --> BaseVariable --> ScriptableObject

This way, we can actually have an array or a list of BaseVariables to hold variables of any type.

To hold the list of all the variables in the project, let’s create another ScriptableObject – Variable Manager.

[CreateAssetMenu(fileName = "varManager", menuName = "Variables/Variable Manager")]
public class VariableManager : ScriptableObject
{
	public List<BaseVariable> Variables = new List<BaseVariable>();

	private void OnEnable()
	{
#if UNITY_EDITOR
		Variables = new List(GetAllVariables());
#endif
	}

	// This method works only in Editor
	private static BaseVariable[] GetAllVariables()
	{
		string[] guids = AssetDatabase.FindAssets("t:BaseVariable");
		BaseVariable[] vars = new BaseVariable[guids.Length];
		for (int i = 0; i < guids.Length; i++)
		{
			string path = AssetDatabase.GUIDToAssetPath(guids[i]);
			vars[i] = AssetDatabase.LoadAssetAtPath(path);
		}
		return vars;
	}
}

inspector1

If you create a couple of variables of different types and run this, you should see a nice list with all your variables in the inspector of the Variable Manager. The list will stay populated even after you quit the play mode, because the data gets saved directly to the scriptable object.

You just have to remember to always run your game in the editor at least once before building your game whenever you add or remove any variables.

Now how to save and load the runtime values of all these variables? We need to somehow serialize and deserialize these different values. As it turns out, when working with a BinaryFormatter you can safely use the base class and it will correctly save all the data from the subclasses.  We can’t throw the whole scriptable object into the formatter, so let’s create a data class:

[Serializable]
public abstract class VariableData
{
}

And extend our generic Variable class so that every Variable type has a corresponding inner Data class.

	[Serializable]
	private class VariableData<TV> : VariableData
	{
		public TV Value;
	}
	
	public override VariableData GetData()
	{
		return new VariableData
		{
			Value = RuntimeValue
		};
	}

	public override void LoadFromData(VariableData data)
	{
		RuntimeValue = ((VariableData<T>) data).Value;
	}

Note: You should reuse the data object instead of creating a new one every time.

Now we can iterate over all of the variables, get their data, cast them to VariableBase, and save them with BinaryFormatter.

Note: Using BinaryFormatter means we need to constrain our variable types. Vector3, Color, and other Unity specific types will not serialize this way.

The problem is how to connect the values to the scriptable objects. Any references will not be serialized. We need to assign every variable a unique ID and then store all data as an (ID, VariableData) pair. We could generate our own ID, but Unity already has a globally unique ID (guid) for every scriptable object, so we can just use that. Extending our GetAllVariables function like this:

	private static BaseVariable[] GetAllVariables()
	{
		string[] guids = AssetDatabase.FindAssets("t:BaseVariable");
		BaseVariable[] vars = new BaseVariable[guids.Length];
		for (int i = 0; i < guids.Length; i++)
		{
			string path = AssetDatabase.GUIDToAssetPath(guids[i]);
			vars[i] = AssetDatabase.LoadAssetAtPath(path);
			if (string.IsNullOrEmpty(vars[i].Guid))
			{
				vars[i].Guid = guids[i];
			}
		}
		return vars;
	}

To wrap all this data and the basic methods, let’s create a new data class:

[Serializable]
public class VariableRevision {
	
	[SerializeField] Dictionary<string, VariableData> Data = new Dictionary<string, VariableData>();
	
	public void LoadFromList(List<BaseVariable> list)
	{
		Data.Clear();
		foreach (var v in list)
		{
			Data.Add(v.Guid, v.GetData());
		}
	}

	public void RestoreVariable(List<BaseVariable> list)
	{
		foreach (var v in list)
		{
			VariableData d;
			if (Data.TryGetValue(v.Guid, out d))
			{
				v.LoadFromData(d);
			}
		}
	}
}

Now we have all the pieces to write a Persistence Manager script that can save and load all variables.

public class VariablePersistence : MonoBehaviour
{
	public VariableManager VariableManager;

	private const string SavePath = "Temp/save.savefile";

	private void Start()
	{
		if (VariableManager == null)
		{
			Debug.LogError("No variable manager assigned!");
			return;
		}
#if UNITY_EDITOR
		VariableManager.RefreshList();
#endif
	}

	public void Save()
	{
		var revision = new VariableRevision();
		revision.LoadFromList(VariableManager.Variables);

		BinaryFormatter bf = new BinaryFormatter();
		FileStream file = File.Create(SavePath);
		bf.Serialize(file, revision);
		file.Close();
	}

	public void Load()
	{
		if (File.Exists(SavePath))
		{
			BinaryFormatter bf = new BinaryFormatter();
			FileStream file = File.Open(SavePath, FileMode.Open);
			var revision = (VariableRevision) bf.Deserialize(file);
			revision.RestoreVariable(VariableManager.Variables);
			file.Close();
		}
	}
}

The neat thing here is that we give BinaryFormatter a dictionary declared with only the (empty) VariableData type and it will correctly (de)serialize all of our properties, no matter if it’s a single int or some complicated data structure.

Check out the full code with a working demo on my GitHub!

Let me know what you think about this solution in the comments, I’m always happy to learn more!

7 thoughts on “Scriptable Object Variables

  1. Thank you so very much for this! Exactly what I needed, clearly explained and with a working demo to boot. You have taught me some important concepts and saved me untold hours of struggle.

    For other amateurs who are implementing this, be aware that the sample code above contains some minor omissions. The full working code is in the GitHub demo.

  2. I just worked out you need to use EditorUtility.SetDirty() after changing SO field values via script during play, or your changes will be lost between editor sessions. So after you update Variables and each vars[i] in VariableManager, add EditorUtility.SetDirty(Variables/vars[i]) so Unity knows to save your variable list / guid when you quit the editor.

  3. Hello, I’m trying to create my scriptableObject architecture. Currently I’m trying to create a :
    ListVariable : Variable<List>
    I wanted to know if it’s possible to do that.
    Quentin.

  4. Hi I’ve been looking for a way to save scriptable objects binary and this is the best thing I encountered.
    Your code seems to not be beginner friendly though, I’m not sure how to even get the Value from the Scriptable Object, how would I lets say get data to display in text field, in this example ( https://unity.com/how-to/architect-game-code-scriptable-objects) you would simply do text.txt = Hp.Value.ToString(); to get the value of ScriptableObject(HP) you want.
    https://github.com/roboryantron/Unite2017
    This one is more simplified but I’m struggling to implement your Save/Load with what was done here, would appreciate some tips how to implement save/load

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s