Harnessing the Power of Scriptable Objects in Unity
Introduction
Managing game state effectively is a critical aspect of game development. In Unity, one powerful but often overlooked feature for this purpose is Scriptable Objects. In this article, I’ll walk you through how I use Scriptable Objects to manage various game states like Boolean flags, Integers, Floats, and Strings in my 2D Metroidvania game, WIZ.
Why Scriptable Objects?
Scriptable Objects offer a lightweight, flexible way to store game data. Unlike MonoBehaviour, Scriptable Objects don’t require a GameObject to sit on, saving precious computational resources.
Our Classes: BoolValue, FloatValue, IntValue, and StringValue
Let’s jump straight into the code. I’ve created distinct classes for different data types: BoolValue
, FloatValue
, IntValue
, and StringValue
. Each class serves as a container for a specific type of data, making it easier to manage various game states.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[CreateAssetMenu]
public class BoolValue : ScriptableObject, ISerializationCallbackReceiver {
public bool initialValue;
public bool runTimeValue;
public void OnBeforeSerialize() {
initialValue = runTimeValue;
}
public void OnAfterDeserialize() {
runTimeValue = initialValue;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[CreateAssetMenu]
public class IntValue : ScriptableObject, ISerializationCallbackReceiver {
public int initialValue;
public int runTimeValue;
public int maxValue;
public void OnBeforeSerialize() {
initialValue = runTimeValue;
maxValue = maxValue;
}
public void OnAfterDeserialize() {
runTimeValue = initialValue;
maxValue = maxValue;
}
}
[CreateAssetMenu]
is useful for allowing these classes to be created directly from the Unity Editor. This makes it easy to manage these data containers.
Serialization and Runtime Values
ISerializationCallbackReceiver
lets us control how these objects are saved and loaded. Each class implements the ISerializationCallbackReceiver
interface. This allows me to easily save and load the game state. The runTimeValue
is checked during gameplay, and it gets stored as initialValue
during saving. When loading a save file, initialValue
repopulates runTimeValue
.
The Special Case of MaxValue
For certain states, like health, the classes have a maxValue
property. This is useful for scenarios like heart containers in adventure games. Here, runTimeValue
represents the current health, initialValue
stores the last saved health, and maxValue
represents the maximum amount of health the player can have.
Using Scriptable Objects in Gameplay
During gameplay, whenever we need to access any value, we query its runTimeValue
. For instance, if we need to check if a door is open or not, we'd check the runTimeValue
of a BoolValue
object representing that door's state.
Saving and Loading
As mentioned before, Scriptable Objects make it easy to save and load game states. Here’s how it works in practice:
- Saving: Before saving,
runTimeValue
is copied toinitialValue
. - Loading: When loading a saved game,
initialValue
is copied back torunTimeValue
.
Conclusion
Scriptable Objects offer a compelling means of managing game state in Unity. By crafting individual classes for each data type and leveraging Unity’s built-in serialization, you can create a smooth, modular system for juggling various game states.
📜 In a follow-up article, I take this concept further by introducing a Scriptable Objects Manager. This manager utilizes a Singleton Pattern to automatically load all Scriptable Objects from the resources folder. It also provides an intuitive API for getting or setting values from other scripts. By marrying the flexibility of Scriptable Objects with the Singleton pattern, this manager elevates our state management to the next level.