Unity Workflows – The Variable Asset

A Unity workflow I see mentioned every now and then, and one that I have grown accustomed to recently, both in personal projects, and during the daily business at Fantastic, yes, is the Variable Asset.

The variable asset is essentially just a ScriptableObject that holds a value.

At first, it seems like a weird way to store data. To some people, creating an asset just to hold one value may sound slightly insane, but once you start toying around with the variable asset, you realize all the crazy flexible things you can do with it!

Let’s have a look at the base implementation.

 

 

using UnityEngine;

public abstract class VariableAsset<T> : ScriptableObject
{
	public const string c_MenuName = "Variable Assets/";
	public delegate void OnValueChanged(T toValue, T fromValue, Object changedBy);
	private OnValueChanged m_OnChanged;

	[SerializeField]
	protected T m_InitialValue;
	[SerializeField]
	protected T m_Value;

	private void OnEnable()
	{
		ResetValue();
	}

	public void ResetValue()
	{
		m_Value = m_InitialValue;
	}

	public T Get()
	{
		return m_Value;
	}

	public void Set(T newValue, bool bInvokeChanged = true, Object changedBy = null)
	{
		if ((m_Value != null && m_Value.Equals(newValue)) || (m_Value == null && newValue == null))
			return;

		T oldValue = m_Value;
		m_Value = newValue;

		if (m_OnChanged != null && bInvokeChanged)
			m_OnChanged.Invoke(m_Value, oldValue, changedBy);
	}

	public void Register(OnValueChanged onChanged)
	{
		m_OnChanged += onChanged;
	}

	public void Unregister(OnValueChanged onChanged)
	{
		m_OnChanged -= onChanged;
	}
}

 

It’s an asset that holds an initial value, a current value, and my personal favourite thing – the option to register for an event when the value of the asset changes, which allows anything that needs to react to the value of the asset, to efficiently do so, without having to continuously check the value!

Here is a very basic example of how the “IntegerAsset” and “BooleanAsset” could look.

using UnityEngine;

[CreateAssetMenu(menuName = c_MenuName + "Integer")]
public class IntegerAsset : VariableAsset<int>
{
	//	We declare GetValue and SetValue so that UnityEvents pick up the methods.
	//	The extra set can also be used to validate potential clamping of the value

	public void SetValue(int value)
	{
		Set(value);
	}

	public int GetValue()
	{
		return Get();
	}

	public void Add(int add)
	{
		Set(Get() + add);
	}

	public void Subtract(int subtract)
	{
		Set(Get() - subtract);
	}

	public void Multiply(int multiplier)
	{
		Set(Get() * multiplier);
	}

	public void Divide(int divisor)
	{
		if (divisor == 0)
		{
			Debug.LogWarning("Cannot divide by zero on integer asset! " + name);
			return;
		}
		Set(Get() / divisor);
	}
}
using UnityEngine;

[CreateAssetMenu(menuName = c_MenuName + "Boolean")]
public class BooleanAsset : VariableAsset<bool>
{
	public bool GetValue()
	{
		return Get();
	}

	public void SetValue(bool bValue)
	{
		Set(bValue);
	}

	public void Flip()
	{
		Set(!Get());
	}
}
&nbsp;

For numeric values, we could implement things like clamping and wrapping of the value, bitwise operations and much more.

Other notable possible asset types include Vector3, Vector2 (and their respective integer versions), String, float, and even Color.

Now let’s dig into some of the useful things they can be used for. One of the more obvious (and what I think is the most useful) use cases is “Game states”.

Has the player visited the goblin cave? Is the game currently paused? Are we currently showing the player a cinematic?

In many cases, developers would choose to expose these values in their respective controllers, managers, singletons and what have you, and then access those variables directly, creating a great many of class dependencies throughout the project.

All of the above questions could instead be answered by Boolean assets, that the appropriate classes assign values to, and everything that will ever have to react to that information can do so through the OnValueChanged delegate.

In the past few months, while working on Returner Zhero, which is a very state-driven puzzle game, one of the most time saving implementations we came up with was a component that would invoke user assigned actions, when a collection of Boolean assets evaluates to a result of either true or false against their respective comparison values combined.

Every time one of the Boolean assets in the collection changes, the collection will re-evaluate, and the user has the option to invoke the “On true” event and “On false” event, either on each individual evaluation, or only when the result of the evaluation changes.

An implementation that triggers UnityEvents could look like this:

using UnityEngine;
using UnityEngine.Events;

public class EventsOnBooleans : MonoBehaviour
{
	private enum InvokeOn { STATE_CHANGED, EVERY_EVALUATION }

	[Header("Settings"), SerializeField, ToggleButton("True", "False")]
	private bool m_InitialState = false;
	[SerializeField, ToggleButton]
	private bool m_EvaluateOnStart = false;
	[SerializeField]
	private InvokeOn m_InvokeOn;
	[Header("Events"),SerializeField]
	private UnityEvent m_OnTrue;
	[SerializeField]
	private UnityEvent m_OnFalse;
	[SerializeField]
	private BooleanComparison[] m_BooleanComparison;

	private bool m_InternalState;

	private void Awake()
	{
		m_InternalState = m_InitialState;

		foreach (BooleanComparison comparison in m_BooleanComparison)
		{
			if (!comparison.Register(OnComparisonUpdated))
			{
				Debug.LogError("Registration has been skipped for a BooleanComparison with missing Boolean Asset reference at GameObject: " + name);
			}
		}
	}

	private void Start()
	{
		if (m_EvaluateOnStart)
		{
			OnComparisonUpdated();
		}
	}

	private void OnComparisonUpdated()
	{
		bool bResult = true;

		foreach (BooleanComparison comparison in m_BooleanComparison)
		{
			bool bComparison;
			if (comparison.Evaluate(out bComparison) && !bComparison)
			{
				bResult = false;
				break;
			}
		}

		if ((m_InvokeOn == InvokeOn.STATE_CHANGED && bResult != m_InternalState) || m_InvokeOn == InvokeOn.EVERY_EVALUATION)
		{
			if (bResult)
			{
				m_OnTrue.Invoke();
			}
			else
			{
				m_OnFalse.Invoke();
			}
		}

		m_InternalState = bResult;
	}
}
using UnityEngine;
using System;

[Serializable]
public class BooleanComparison
{
	[SerializeField]
	private BooleanAsset m_BooleanAsset;
	[SerializeField, ToggleButton("True", "False")]
	private bool m_CompareValue;

	private Action m_OnValueChanged;

	//Returns whether the evaluation is valid.
	public bool Evaluate(out bool bResult)
	{
		if (m_BooleanAsset == null)
		{
			bResult = false;
			return false;
		}

		bResult = m_BooleanAsset.Get() == m_CompareValue;
		return true;
	}

	private void OnValueChanged(object bNewValue, object bOldValue, UnityEngine.Object changedBy)
	{
		if (m_OnValueChanged != null)
		{
			m_OnValueChanged.Invoke();
		}
	}

	public bool Register(Action onUpdated)
	{
		if (m_BooleanAsset == null)
		{
			return false;
		}

		//First registrar makes us register to the BooleanAsset
		if (m_OnValueChanged == null && onUpdated != null)
		{
			m_BooleanAsset.Register(OnValueChanged);
		}

		m_OnValueChanged += onUpdated;
		return true;
	}

	public void Unregister(Action onUpdated)
	{
		if (m_BooleanAsset == null)
		{
			return;
		}

		m_OnValueChanged -= onUpdated;

		//If no registrars are left, unregister from the asset
		if (m_OnValueChanged == null)
		{
			m_BooleanAsset.Unregister(OnValueChanged);
		}
	}
}

And it ends up looking like this in the editor:

Note that this implementation assumes “And” expressions between the Boolean assets.

A more flexible implementation could also allow for evaluating “Or” expressions.

Also, we make use of a custom “ToggleButton” attribute and an editor script.

Check out this GitHub repository for the full implementation, along with more thorough variable asset implementations!

Another benefit of using variable assets is the ability to access the same data across multiple scenes. With additive scenes becoming increasingly popular (for good reasons), ScriptableObjects, in general, are ideal data containers. Imagine having a component that rotates an object on user interaction in one scene, and stores the velocity of the rotation in a float asset.

In an entirely different scene, audio could adjust itself to this float value simply by reading it from the asset. Another piece of audio could react to a float value being populated by an entirely different type of game interaction.

This is just one of many “Input/Output” use cases, with the biggest advantage being less mingling between classes in code.

If we want these variables to persist through game sessions, we only need to assign a GUID to each asset, point to the variable assets that need saving from some utility that can serialize them in our language of choice (JSON, YAML etc.)

Most workflows have their drawbacks, however, and this one is not an exception.

There are some important things to keep in mind when working with the variable asset.

I do not consider the workflow very suitable for data that needs to be created and maintained at runtime en masse, but rather for data that is defined with an initial value at edit time and may or may not be modified at runtime.

Since anything can point to a variable asset and change its value, it is vital to be disciplined in project structure, as to not confuse variable assets with one another, or change them from unintended places. Most of the time, only one source should change a variable asset, whereas multiple objects can safely read from it.

Ideally, the project can somehow visualize its dependency chain, so that we can more easily identify where unintended references occur.

A very effective approach to limiting project clutter with variable assets is to create a “wrapper asset” that will auto-generate variable assets as sub-assets of itself.

This way, many assets suddenly become one asset that can still be inspected as the individual assets, which means just one file for version control, and that naming can be automatic!

Finally, with this workflow, we need to be especially careful in games with re-playable elements. If a scene is re-loaded, we also have to make sure to reset our variable assets to their initial state when necessary. Sadly we cannot rely on Unity to reset the assets for us, even though they are completely dereferenced by our code.

In the end, I would certainly recommend this workflow for any project that is heavily state and/or data-driven, given that the project structure is maintained well.

Again – for a more in-depth implementation, including more asset types and functionality, see Casper’s GitHub repository!

 

 

Learn more about the author: https://connect.unity.com/u/58502e0732b3060024578e1a


		
We have placed cookies on your computer to help make this website better. Read the cookies policy
yes, I accept the cookies