Exploring Scriptable Objects in Unity Game Development

Published

Scriptable Objects are a concept specific to Unity, enabling you to create modular and flexible code while improving collaborative workflow. We’ll go in depths into when and how to use these scriptable objects in your workflow and what benefits they bring to your development efforts.


What are Scriptable Objects in Unity

Scriptable Objects in Unity can be likened to specialized data storage units tailored for game development. They serve as containers for holding game-related information, such as settings, behaviors, and mechanics. Similar to how a storage unit holds items, Scriptable Objects store game data, offering the advantage of direct editing within Unity’s editor, support for real-time changes which can be persisted across sessions. Finally it also gives the ability to encapsulate reusable logic that can be called upon by various game systems.


Why you should use Scriptable Objects

Scriptable Objects are unique to the Unity Engine and provide a variety of interesting features and benefits. They are also extremely versatile: there are many ways in which Scriptable Objects can be used when developing your own game in Unity.


Scriptable Objects as Native Data Containers

The first use is as data containers. Data for games and software is usually contained in files with formats such as JSON, YAML or CSV, which are not natively supported: this data must first be read then converted for use in a struct or a class. Scriptable Objects are different: they are native to the Unity Engine so they can be drag and dropped wherever in your code and you don’t need to implement any sort of serialization / deserialization step.

The fact that they are native types also enables you to create and modify them directly in the Editor, just as you would with a Monobehaviour object. This also means that you can also implement custom editor windows or inspectors for these Scriptable Objects.

Finally, they provide the ability to store data in a persistent manner, which can be great news for development: while Unity rolls back every changes to parameters of Monobehaviours when exiting Play Mode, these Scriptable Objects can keep track of changes and simplify the whole process of calibrating your game instead of manually keeping track of all the changes you’ve made while looking for the perfect camera angle or animation duration.


Decoupling Logic and Data

In traditional programming, logic and data are often tightly intertwined within classes or scripts. This can lead to code that’s hard to maintain, modify, and extend. Scriptable Objects address this by allowing you to create separate assets solely for storing data (the Scriptable Object) and for housing the logic (scripts that interact with the Scriptable Object, usually a MonoBehaviour).

With logic and data decoupled, you can change the behavior of a game system by simply swapping out the Scriptable Object instance. For example, you could have a “Weapon” Scriptable Object and different “AttackBehavior” Scriptable Objects. Swapping the AttackBehavior Scriptable Object can drastically alter how the weapon behaves, all without touching the weapon’s code. Scriptable Objects can also inherit from other classes and implement interfaces, which again helps with code reuse.

This separation of concerns also facilitates collaborative work: programmers can focus on creating new mechanics and implementing game logic while designers can fine-tune these mechanics to maximize player enjoyment.


Cross-system communication

Scriptable Objects can be used to facilitate communication between different parts of your game. You can create Scriptable Objects that act as message hubs, broadcasting events to various listeners when specific actions or conditions are met.

Let’s take an example. When the player takes a hit (in a RPG or FPS game), several systems need to react to this event: the UI needs to show the new HP value, there may be a special animation to be played when the player receives damage, some actions might become blocked for a few seconds in the player controller’s state machine, or there may be some post-processing effects triggered (for example a red vignette is shown).

There can be quite a few systems that needs to be notified that the player HP has changed so that they can react appropriately, and it’s not a good practice to link all these systems together. In this case, a good architectural decision is to use events: different systems can be notified of changes in a certain value while not knowing anything about the other systems tracking this same variable. To implement this, these systems may get access to the same Scriptable Object instance and register as listeners to an event. We’ll see below how to create an Event System using Scriptable Objects.


What are common use cases for Scriptable Objects

Scriptable Objects in Unity are incredibly versatile and can be used in various ways across different aspects of game development. Some common use cases for Scriptable Objects include:

Game Settings

Scriptable Objects are excellent for storing and managing game settings such as graphics options, sound preferences, and input configurations. These settings can be easily customized by players without altering the underlying code.

Character Stats

Utilize Scriptable Objects to define character attributes such as health, damage, and abilities. In complex scenarios, where multiple systems need to react to changes in character stats, Scriptable Objects provide a decoupled way to achieve this coordination without creating tight dependencies.

For instance, when a player takes a hit, various actions might occur simultaneously, including playing a hit animation, triggering sound and visual effects, updating UI elements, and adjusting the character’s controller state. By using Scriptable Objects, these systems can observe and react to the same value without strong references, promoting modular and maintainable code.

UI Theming

Implement dynamic UI theming using Scriptable Objects. Define color schemes, fonts, and styles within a Scriptable Object and apply them across your user interface. This allows for easy theme swapping and a consistent visual experience without the need to modify UI code directly.

Inventory and Crafting

Create an organized inventory and crafting system using Scriptable Objects. Each item or recipe can be represented by a Scriptable Object, enabling efficient management of data, interactions, and item behavior. This separation between data and behavior enhances code clarity and maintainability.

Event Systems

Scriptable Objects are invaluable for designing efficient event systems. These systems enable communication between different parts of your game without tight coupling. Game events, such as player actions, enemy spawns, or dialogue triggers, can be implemented using Scriptable Object events, providing a modular and decoupled approach to event-driven architecture.


Scriptable objects represent a new practical way to approach common problems when creating your own game. They are quite versatile and can be plugged in many systems: they will definitely fit at least some of your needs when designing your game.

How to create a new Scriptable Object

To use Scriptable Objects in Unity, you must first define the object’s properties and behaviour just like you would with any C# class:

  1. Create a class for your scriptable object by right-clicking on the project panel, and choose “Create” then “C# Script”.
  2. Define your class and the properties you want to expose
  3. Have your class inherit from the ScriptableObject class
  4. Define a CreateAssetMenu attribute
  5. Right-click on the project panel, “Create” and choose your newly defined scriptable object according to the CreateAssetMenu attribute
  6. You now have an instance of the ScriptableObject that you can use in other classes

For example, here is the definition for a Scriptable Object that would track an entity’s or player’s health points and mana points.

using System;
using UnityEngine;

[CreateAssetMenu(fileName = "SO_Stats", menuName = "Noveltech_ScriptableObjects/SO_Stats", order = 0)]
public class SO_Stats : ScriptableObject
{
    public int health_points;
    public int mana_points;
}

This scriptable object can then be used in any other class, including a MonoBehaviour. Let’s create a basic player controller that checks the player’s hp at every tick and calls a method when they reach 0.

using System;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public SO_Stats player_stats;

    void Update() {
        if (player_stats.health_points == 0) {
            OnDeath();
        }
    }

    void OnDeath() {
        // not implemented
    }
}

An advantage of scriptable objects is that this object can be used in multiple instances or systems so this can help reduce memory usage.


Encapsulating Scriptable Object State: Protecting Fields and Enforcing Access with Interfaces

While being able to share a single instance of a ScriptableObject between systems is great, this brings additional challenges: the state is exposed to all systems who have access to the object, meaning that a consumer system has mutable access to the Scriptable Object, which is not desirable. A workaround is then to set the fields as private and to create getters and setters for the associated data.

Let’s modify our Scriptable Object for character stats to set fields as private and create getters and setters.

using System;
using UnityEngine;

[CreateAssetMenu(fileName = "SO_Stats", menuName = "Noveltech_ScriptableObjects/SO_Stats", order = 0)]
public class SO_Stats : ScriptableObject
{
    private int health_points;
    private int mana_points;

    public int GetHp() {
        return health_points;
    }

    public void SetHp(int new_hp) {
        health_points = new_hp;
    }
}

Now that we have these accessors and fields are private, this does not completely solve our problem: now consumers can still modify state through the setter function. We can use interfaces to avoid this and protect data, seperating between consumers and providers.


public interface IHpReader
{
    public int GetHp();
}

public interface IHpWriter
{
    public void SetHp(int new_value);
}

Now, data access can be gated using typing with interfaces instead of using the Scriptable Object type. Let’s create a class that’s checking the HP at each tick and logs a warning if hp is below a certain level.

public class HpConsumer: MonoBehaviour {
    public IHpReader mHpStats;
    public int mThreshold;

    void Update() {
        if (mHpStats.GetHp() < mThreshold) {
            Debug.Log("Health points are low!");
        }
    }
}

The class can read the HP but cannot modify them. This allows you to enforce non-mutability of state at different points in your logic.

It’s important to note though that the Unity Editor does not allow to set Interfaces objects directly in property fields. If you want to do this, you’ll need to add an intermediary variable and cast it to the desired Interface type.


using UnityEngine;
using UnityEngine.Scripting;

public class UIController : MonoBehaviour
{
    public ScriptableObject interface_stats;
    private IHpWatcher _interface_stats;

    private void OnValidate()
    {
        if (interface_stats != null && interface_stats is IHpWatcher)
        {
            _interface_stats = (IHpWatcher)interface_stats;
            return;
        }

        interface_stats = null;
    }
}

The OnValidate method is called every time a property is modified in the UnityEditor. With our code, we’re checking that the “interface_stats” has been assigned to and that the assigned object is a ScriptableObject that implements the IHpWatcher method.


Implementing an Event System with Scriptable Objects for Controlled State Management

Using Scriptable Objects to create an event system provides a clean and interesting set up to facilitate cross-system communication.

We can set up a Scriptable Objects so that MonoBehaviours and other scripts can register a callback when an event or action is called. For example, let’s reuse our previous scriptable object for character stats and add an event “OnPlayerHpChanged” which is triggered when the Health Points of the player is updated. For good measure we’ll also add a “OnPlayerMpChanged” for Mana Points.

using System;
using UnityEngine;

[CreateAssetMenu(fileName = "SO_Stats", menuName = "Noveltech_ScriptableObjects/SO_Stats", order = 0)]
public class SO_Stats : ScriptableObject
{
    private int health_points;
    private int mana_points;

    public event Action<int> OnPlayerHpChanged;
    public event Action<int> OnPlayerMpChanged;


    public void SetHp(int new_hp)
    {
        health_points = new_hp;
        OnPlayerHpChanged?.Invoke(health_points);
    }
   
    public void SetMp(int new_mp)
    {
        mana_points = new_mp;
        OnPlayerMpChanged?.Invoke(mana_points);
    }
}

We’ve now added Actions that return an integer when invoked, and our setter methods will trigger these events when HP or MP are changed. We can now have MonoBehaviour objects take a reference to this scriptable object, register a callback to the event and be notified when the value has changed.

using UnityEngine;


public class UIController : MonoBehaviour
{
    public SO_Stats player_stats;

    private void OnEnable() {
        player_stats.OnPlayerHpChanged += OnPlayerHpChanged;
    }

    private void OnPlayerHpChanged(int new_value) {
        Debug.Log(string.Format("Hp Changed to {0}", new_value));
    }
}

Now when SetHealth is called on the SO_Stats Scriptable Object, the callbacks registered by the listeners will be executed, with the new value of health passed to the calls. In our case, we’re just logging the new value of health points, but you can obviously do more complex logic.

You could also pass the previous value of HP along the new one, or use interfaces to prevent consumers from getting mutable access to the Scriptable Object and only be able to register to events.


In conclusion, Scriptable Objects are a very versatile tool that has a great role to play in the Unity Gamedev’s toolkit. It enables you to create native data containers that you can modify directly in the Editor, but also create modular logic that can be shared between systems. Finally, it’s also great for cross-system communication, as Scriptable Objects allow you to easily implement event systems.