Skip to main content

Scriptable Objects

A ScriptableObject (abbreviated as SO) is a data container that you can use to save large amounts of data, independent of class instances. An example scenario where this will be useful is when your game needs to instantiate tons of Prefab with a Script component that stores unchanging variables. We can save memory by storing these data in a ScriptableObject instead and these Prefabs can refer to the content of the ScriptableObject at runtime.

ScriptableObject is also useful to store standard constants for your game, such as reset state, max health for each character, cost of items, etc.

In the later weeks, we will also learn how to utilise ScriptableObjects to create a handy Finite State Machine.

From the official documentation, it's stated that the main use cases for ScriptableObjects are:

  • Saving and storing data during an Editor session
  • Saving data as an Asset in your Project to use at run time

This week, we mainly focus on the simplified version of the first use case: to use an SO instance to store game constants, accessible by any script.

danger

Scriptable objects are mainly used as assets in a project that can be referenced by other assets in the project, and they are serialised into your project. However, they cannot be modified permanently in the exported build. Any changes would be reverted when you restart your game as scriptable object are serialised into the asset database and such assets can not be changed at runtime.

You can change the data it contains, and it will persist throughout your game (between scenes, etc) but you cannot expect it to persist upon restart in your exported build!

In editor, changes stored in SO persist even after you stop and restart the game so they behave differently from export build!

To properly save various game data, you can use Unity's Binary Serialization, PlayerPrefs, basic Serialization with JSON files, or paid asset like EasySave. There are many ways depending on the complexity of the data you save: simple settings like volume level, difficulty level, or primitive data type like int, string, float, or more complex stuffs like an array.

Scriptable Object Template

To begin creating this data container, create a new script under a new directory: Assets/Scripts/ScriptableObjects/and call it GameConstants.cs. Instead of inheriting MonoBehavior as usual, we let it inherit ScriptableObject:

GameConstants.cs
using UnityEngine;

[CreateAssetMenu(fileName = "GameConstants", menuName = "ScriptableObjects/GameConstants", order = 1)]
public class GameConstants : ScriptableObject
{
// set your data here
}

The header CreateAssetMenu allows us to create instances of this class in the Project in the Unity Project Window. Proceed by declaring a few constants that might be useful for your project inside the class, for example:

public  class GameConstants : ScriptableObject
{
// lives
public int maxLives;

// Mario's movement
public int speed;
public int maxSpeed;
public int upSpeed;
public int deathImpulse;
public Vector3 marioStartingPosition;

// Goomba's movement
public float goombaPatrolTime;
public float goombaMaxOffset;
}

Instantiate

Now you can instantiate the scriptable object by right clicking on the Project window then >> Create >> ScriptableObjects >> GameConstants (this is possible since we have declared CreateAssetMenu). Give it a name, here we call it SMBConstants (SuperMarioBrosConstants).

Over at the inspector, you can set the values to correspond to each constant that's been declared before in PlayerMovement and EnemyMovement.

As of now, maxLives are not used yet, leave it at 10.

info

The values stored inside a ScriptableObject persists (unlike runtime variables that exists only in-memory), so you can store something in these data containers such as the player’s highest score, and load it again the next time the game starts. You can treat SO instances as files.

Usage in Runtime

To use the SO values in any script, simply declare it as a public variable and link it up in the inspector. For example, we can modify PlayerMovement.cs as follows:

PlayerMovement.cs
public class PlayerMovement : MonoBehaviour, IPowerupApplicable
{
public GameConstants gameConstants;
float deathImpulse;
float upSpeed;
float maxSpeed;
float speed;

// other attributes

// Start is called before the first frame update
void Start()
{
// Set constants
speed = gameConstants.speed;
maxSpeed = gameConstants.maxSpeed;
deathImpulse = gameConstants.deathImpulse;
upSpeed = gameConstants.upSpeed;
// Set to be 30 FPS
Application.targetFrameRate = 30;
marioBody = GetComponent<Rigidbody2D>();
marioSprite = GetComponent<SpriteRenderer>();

// update animator state
marioAnimator.SetBool("onGround", onGroundState);

}

}

By using SO as data container for your game constants, you can easily modify them later during testing stage without having to touch your scripts.

Methods

You can also write regular methods in an SO. For instance, you can create an SO that represents a Game Event:

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "GameEvent", menuName = "ScriptableObjects/GameEvent", order = 3)]
public class GameEvent : ScriptableObject
{
private readonly List<GameEventListener> eventListeners =
new List<GameEventListener>();

public void Raise()
{
for(int i = eventListeners.Count -1; i >= 0; i--)
eventListeners[i].OnEventRaised();
}

public void RegisterListener(GameEventListener listener)
{
if (!eventListeners.Contains(listener))
eventListeners.Add(listener);
}

public void UnregisterListener(GameEventListener listener)
{
if (eventListeners.Contains(listener))
eventListeners.Remove(listener);
}
}

You can then create instances of these events, such as: PlayerDeathEvent or ScoreIncreasedEvent and use it in a Script (in place of Singleton pattern). An SO can also be used to represent a state, e.g: CurrentScore if that state is meant to be shared by many instances in the game (read). This allows you to retain the score of your current progress should you exit the game. We will learn more about this next week when we dive deeper into Scriptable Object Game Architecture.

Storing Game States or Variables During Editing for Faster Development

In this section, we will mainly discuss the role of SO instances as persistent variable storage in the editor. We can write custom getters and setters to make it more convenient to manage.

info

Do not treat SO as Files

SO are saved as assets in the project (.asset files). Any changes made during runtime in the Editor can persist because Unity writes data back into .asset file. However in a built game, SO do NOT save data between sessions. Once you quit the game, the SO resets to its original values. To save persistent data across sessions in built games, you need to save to a file (JSON, PlayerPrefs).

Scriptable Objects (SOs) are perfect for storing and tweaking variables during development, because they act as editable assets in the Unity Editor.

C# Method Overloading

There are many states in the game that should be shared among different instances, such as whether Mario is alive or dead, current game score (for combo system if possible), where Mario currently is (which World to indicate progress), and many more. We can utilise SO by creating a generic variable container as follows:

Variable.cs

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


public abstract class Variable<T> : ScriptableObject
{
#if UNITY_EDITOR
[Multiline]
public string DeveloperDescription = "";
#endif

protected T _value;
public T Value
{
get
{
return _value;
}
set
{
SetValue(value);

}
}

public abstract void SetValue(T value);

}

Then, we can create a subclass called IntVariable (similarly you can create other variable types as well) that also store its highest value thus far:

IntVariable.cs
using UnityEngine;

[CreateAssetMenu(fileName = "IntVariable", menuName = "ScriptableObjects/IntVariable", order = 2)]
public class IntVariable : Variable<int>
{

public int previousHighestValue;
public override void SetValue(int value)
{
if (value > previousHighestValue) previousHighestValue = value;

_value = value;
}

// overload
public void SetValue(IntVariable value)
{
SetValue(value.Value);
}

public void ApplyChange(int amount)
{
this.Value += amount;
}

public void ApplyChange(IntVariable amount)
{
ApplyChange(amount.Value);
}

public void ResetHighestValue()
{
previousHighestValue = 0;
}

}

Finally, you can instantiate GameScore from IntVariable. We suggest you organise your directory accordingly:

You can use GameScore in GameManager (Singleton) in favour of a private score variable:

GameManager.cs

public IntVariable gameScore;

// use it as per normal

// reset score
gameScore.Value = 0;

// increase score by 1
gameScore.ApplyChange(1);

// invoke score change event with current score to update HUD
scoreChange.Invoke(gameScore.Value);

This way, we have a centralised container for our score and keeping track of highscore during this gameplay time or in editor. Simply refer to it via another script, for instance:

HUDManager
public class HUDManager : MonoBehaviour
{

public GameObject highscoreText;
public IntVariable gameScore;


public void GameOver()
{
// other instructions
// set highscore
highscoreText.GetComponent<TextMeshProUGUI>().text = "TOP- " + gameScore.previousHighestValue.ToString("D6");
// show
highscoreText.SetActive(true);
}

}

And we can have highscore reported at the end of a run. This value is persistent (even if you stop and start the game again) in the Editor:

However if you would like to make this highscore persistent in build, then as stated, you need to save it to a file. You can use (JSON, PlayerPrefs).

Summary Usage

SOs are data containers used to store and manage structured data without needing a MonoBehaviour. They help keep data organized, reusable, and editable in unity editors. Basic use cases of SOs include:

  1. Configuration data: store game standard game settings (not player preferences!), such as high/low graphics setting
  2. Game balance data: enemy stats, weapon damage, level parameters, numerical formulas that support the economy of the game
  3. Shared data across scenes: UI themes, player inventory, localization text
  4. Protytping and presets: quickly swap weapons, characters, power-ups, reference prefabs
  5. Event and messaging system: Scriptable Object Game Architecture (next lab), to decouple communications between game objects

Do not use SOs for:

  1. Permanent data saving between game runs (close an reopen the build)
  2. Storing scene-specific runtime data since SOs are not per-instance objects but rather they are shared across all instances
  3. Directly holding references to scene objects. SOs exist outside the scenes