Global Variables in Unity with Scriptable Objects

Global variables, the right way.

In game development, you often find that you need to access a certain variable or object from many different places. Things like the score, the player's health, a user-configured setting, and more are frequently needed by many different components throughout the game.

For simple situations, it's perfectly fine to just access a public property directly using a reference set in the inspector or through Unity's find object functions. But direct references sometimes don't make sense or aren't possible, or would be excessively cumbersome to use and maintain.

For these situations, you want a global variable.

What Is A Global Variable And Why Would I Want One

A global variable is a variable that is defined outside of any classes and is accessible to all functions. True global variables aren't possible in Unity, but there are tricks you can use to get the next best thing.

Global variables save time and memory by creating a centralized place to find a necessary piece of data. Further, because the functions are all referencing the same variable, changes to the variable are automatically picked up the next time it is read.

The Non-Unity Way

In many applications, global variables are achieved by creating a static class with the variables as its members.

public static class Globals
{
    public static int score;
    public static Player player;
    public static int playerHP = 100;
}

This is a tried and true method, and can work great for your use case. The variables are not tied to any instance and can be accessed anywhere using the class name instead of an instance reference. For example, the current health of the player is Globals.playerHP.

It might not be ideal for a game built in Unity, however.

One problem with this method is that the variables are hard-coded. Adding new ones or changing the default value of them requires recompilation, and they cannot be created at runtime or changed to reference a different variable.

Another problem is that they are not logically connected; the only thing the variables have in common is that they are all globally accessible. This can be corrected by creating subclasses within the Globals class and organizing the variables there, such as Globals.PlayerData.playerHP.

But most importantly, the biggest problem with this method is that these variables are not accessible to the Unity inspector, which makes working with them much more difficult.

Global Variable Scriptable Object

These problems can be solved with the power of Unity's Scriptable Objects system. Scriptable objects are instances of a class that are instantiated as assets in the filesystem rather than objects in the scene, which makes them ideal for this application.

To simplify the amount of code we need to write and maintain, we will create a base Global Variable scriptable object containing a generic type.

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

/// <summary>
/// Stores a variable for global accessibility.
/// </summary>
/// <typeparam name="T">The type of variable to be stored.</typeparam>
public class GlobalVariable<T> : ScriptableObject
{
    [Tooltip("The current value of this variable at runtime.")]
    [SerializeField] protected T runtimeValue;

/// The current value of the global variable.
    public virtual T Value
    {
        get => runtimeValue;

        set => runtimeValue = value;
    }
}

This object is very simple; it contains a Value property backed by a serialized field.

In C# you can instantiate generics with a specified type at runtime (as you do with List<int> and Dictionary<string, float>, for example), but Unity does not support generics directly and requires concrete types. While you may not be able to use GlobalVariable<T> scriptable objects, you can inherit from them with concrete types usable by Unity while still benefiting from having all code handled in one class.

Create derived classes inheriting from the GlobalVariable class specifying the type of variable you want it to contain. There is no need for a body, as all the code is inherited from the base GlobalVariable class. These can then be instantiated as global variable scriptable objects of that type.

// Global variable containing a boolean.
public class GlobalBool : GlobalVariable<bool>
{
}

// Global variable containing a float.
public class GlobalFloat : GlobalVariable<float>
{
}

// Global variable containing a GameObject reference.
public class GlobalGameObject : GlobalVariable<GameObject>
{
}

GlobalBool could be instantiated as ShowSubtitles, DarkModeEnabled, AimAssist, KeepInventoryOnDeath, or DoFireTick, for example.

To utilize these global variables, all you need to do is define a reference to a GlobalBool or GlobalFloat in the code needing access to the global variable, drop a reference to the scriptable object into the field using the Unity inspector, and access the Value property of the global variable in code.

// Another script sets this global variable to the gameobject under the mouse
[SerializeField] private GlobalGameObject aimTarget;

// Displays the name of the game object under the reticle in a textbox
aimTargetNameDisplay.Text = aimTarget.Value.Name;

Now you have a value that can be read or set by any interested party and shared by many objects, even across scenes! This is a great way to store user settings like UI scale, aim assist strength, and difficulty modes.

Defining a Default Value

There is one problem with the simple global variable class above... the value doesn't reset once changed! If the player was hurt before exiting play mode, you find that they're still hurt when you re-enter play mode, even though you started the level over!

Scriptable Objects, because they are persistent files, are never reset to their original state, even when restarting the game. In some cases, this is desirable, and in others, it's not.

Give the variables an option to reset by storing their default value as a new field, and adding a function to set the runtimeValue to the defaultValue at a chosen stage in the scriptable object life-cycle. A simple boolean can enable or disable this behavior on a per-object basis, if desired.

[SerializeField] protected T defaultValue;
[SerializeField] protected T runtimeValue;
[SerializeField] protected bool doReset = true;

// . . .

private void OnValidate()
{
    // Resets the global variable when being updated in the editor
    ResetValue();
}

private void Awake()
{
    // Resets to default value when the game is loaded
    ResetValue();
}

private void ResetValue()
{
    if (doReset) runtimeValue = defaultValue;
}

Adding ResetValue() to OnValidate() causes the variable to reset whenever changes are made to the object in the Unity inspector.

Adding it to Awake() makes it reset whenever it is initially loaded, which may be anywhere between when the game starts and when the variable is first accessed.

Choose the one that is right for you, or do a boolean for both for even more fine-tuned control.

And there you go! A default value can now be defined and the scriptable object will return to that value once the reset conditions are met.

Variable Change Events

While having multiple scripts accessing the same variable helps the scripts to share information, you may want to go a step further and actively alert interested scripts when the variable changes instead of having them check periodically.

Implementing a property changed event is easy!

Add a delegate to the base GlobalVariable class, and invoke it in the Value property setter. That's all there is to it! Now, all of your global variables have a built in PropertyChanged event to which interested scripts can subscribe, which will be fired every time a change is made to the global variable's value.

public virtual T Value
{
    get => runtimeValue;

    set
    {
        runtimeValue = value;
        OnValueChanged?.Invoke(value);
    }
}

public delegate void ValueChanged(T newValue);
public event ValueChanged OnValueChanged;

You can subscribe a function using the additive compound assignment operator, as you would for any delegate, or use subtractive to unsubscribe.

// registers the UpdateHealthbarFill function to listen for changes to
// the player's health value
playerHealthGlobal.OnValueChanged += UpdateHealthbarFill;

// . . .

public void UpdateHealthbarFill(float newHealth)
{
    // change healthbar fill level here
}

This basically turns the global variable into a scriptable object event channel dedicated to that variable.

An obvious use for this is to have the health bar UI element automatically change in response to a change in the player's hit points, or have the enemy name tag at the top of the screen change to the name of whatever game object is being aimed at with the reticle.

Conclusion

Effectively sharing information between scripts is essential for a successful game. Global variables powered by scriptable objects are a great way to centralize key data points to make sure everything is updated without excessive or cross-scene references.

Did you find this article valuable?

Support Bear Evans by becoming a sponsor. Any amount is appreciated!