Using Unity Scriptable Objects to solve Architecture problems

A lonely sphere

This article covers the basics of Scriptable Objects and some ways in which they’re helpful. First, I’ll explain what Scriptable Objects are. Then, I’ll explain what they do exceptionally well and some of the ways I like to use them. Finally, I’ll give an example problem that I solved using Scriptable Objects.

What are Scriptable Objects?

To put it briefly, a Scriptable Object (SOs) is an asset on which you can attach a script. A Scriptable Object is similar to a Game Object with an attached MonoBehaviour, but with a couple of differences. Game Objects only exist within Scenes or Prefabs, whereas a Scriptable Object is an asset that exists on disk. In that way, they’re more similar to a prefab (which is a file with a .prefab extension) than a Game Object (which only exists within other files). Another difference is that SOs don’t have a hierarchy and can’t be nested the way Game Objects are. One last thing to note is that, unlike Game Objects, each Scriptable Object asset only has a single script attached to it. As I’m sure you know, a single Game Object can have many components attached to it. So, knowing all this, what makes Scriptable Objects worthwhile? In the next section, I’ll explain some of the situations for which they’re well suited.

When do you use Scriptable Objects?

It would not be easy to enumerate every possible use of Scriptable Objects. I hope that by explaining their strengths and how I use them, you’ll see how they solve your particular problems.

At their core, Scriptable Objects are great when you have shared data, especially if you want to assign that data using the Inspector. By the way, shared data includes both code and values. Any number of MonoBehaviours can reference the same Scriptable Object. So, they’re great as a single source of truth. Rather than copying values throughout your code or devising complicated ways to access data through a chain of command, you can store information in the Scriptable Object itself and reference that instead.

Additionally, since they’re Unity assets, you can easily save multiple versions of the same Scriptable Object and swap them out to test different ideas. What I mean by this is, instead of having a MonoBehaviour with several serialized parameters, create a Scriptable Object to hold those serialized parameters instead. Now, your MonoBehaviour only needs to reference a single serialized field for Parameter SO. Doing so allows you to store multiple variations and swap parameters by dragging different SOs via the Inspector. This modular approach enables designers to quickly try lots of ideas and preserve their experiments as well.

As I mentioned, Scriptable Objects can hold code as well. So, another application is to switch out algorithms. For example, you could have several Scriptable Objects, each with a unique OnUpdate method, and swap them out to give objects different behaviours. In this way, you could use SOs as states within a Finite State Machine system, for example.

One interesting characteristic of Scriptable Objects is that they aren’t reset when exiting play mode. Depending on what you’re doing, this could either be a benefit or bothersome. We’ll use it to our advantage, though. Scriptable Objects are a great way to store authoring data. By that, I mean the data used to initialize objects rather than the data they reference at runtime. Doing this means you can tweak initial values at runtime and keep your changes. Another advantage is that because Scriptable Objects are composed of simple data and are stored separately from Scene or Prefab files, it’s much easier to avoid merge conflicts when you’re working with version control. If you’ve ever spent hours tweaking values only to have your changes lost in a messy code merger, you understand how beneficial this is.

So, now we understand the value of Scriptable Objects. In the next section, I’ll explain the example that lead me to write this post.

Health System Example

Recently, my team and I took part in a long-form game jam. This lead to many challenges. In a game jam, validating ideas through rapid prototyping takes precedence over good engineering practices. However, in a month-long game jam, the lack of good engineering practices can cause a lot of friction that gets drawn out over a long time. So what do you do? Iterate and refactor as you go, but also take care not to lose too much time. What I’m about to describe is one such refactor.

While balancing the health and damage values throughout our game, I found it difficult to constantly dig through prefabs to tweak a single number buried deep inside the hierarchy. On other projects, I solved this with a spreadsheet and a scripted importer. In other words, first, insert all the values into a spreadsheet. Then, write an importer to read the values and assign them to the various prefabs. However, because the whole team was working on the same set of prefabs and in the same scenes, it was easy for miscommunication between team members to lead to complicated mergers between prefabs. My point is that even if I went with the spreadsheet approach, it would still tie up the prefabs and preventing others from working on them. So instead, I built a simple system based on Scriptable Objects. Rather than storing values directly in the prefabs, I store values in SOs and reference those in the prefab. It’s quick to implement and avoids all the issues I was experiencing. To do this, we first need a Scriptable Object that holds a single value.

By the way, you might be wondering why not make a single Scriptable Object that holds all the values, like a database. The reason is that we would then need to modify the code to reference the values explicitly, using their variable name. By doing it the way I chose, we can drop any IntValue Scriptable Object in the Inspector, and it’ll work without changing the underlying code.

Here’s what it looks like in practice. We start with a Scriptable Object that holds nothing but an Int:

public class IntValue : ScriptableObject
{
    [SerializeField] int _value;
    public int Value => _value;
}

Then, in our hypothetical HealthSystem, we reference an IntValue Scriptable Object to hold our max health and set its value to our current health.

public class HealthSystem : MonoBehaviour
{
    [SerializeField] IntValue _maxHealth;
    int _currentHealth;

    void Awake()
    {
        _currentHealth = _maxHealth.Value;
    }
}

In this example, I made the int values private so you can only modify them in the Inspector. You could allow changes at runtime, but remember that those changes persist when exiting play mode. So if you choose that option, you may want to reset the values yourself when entering and exiting play mode.

Now, you’re free to create SOs for any values you want. In our project, I made a folder specifically for health and damage values, making it much easier to balance because all the values are in a folder side-by-side.

Additionally, if you later wanted to use a spreadsheet-based approach, you could import the values directly into your Scriptable Objects. You wouldn’t even have to change the underlying code.

There have been presentations (in the links further down) that make Scriptable Objects seem like a silver bullet that will solve all your Architecture issues. And I admit that this SO-based approach worked exceptionally well for this situation. However, every approach has its strengths and weaknesses. You must always consider the specific problem you’re facing and design a solution to solve it. I think Scriptable Objects are a valuable and versatile tool; I use them a lot! But, I don’t use them for everything.

Hopefully, this article gave you some ideas of ways in which you can use Scriptable Objects.

If this article was helpful, join my mailing list, and I’ll notify you about future blog posts. I’ve uploaded a simple example project here on Github. If you want to see more about Scriptable Objects, check out this talk and this talk.

Leave A Comment