10 Ways to Use Scriptable Objects in Unity
ScriptableObjects are data assets. They live in the Project window instead of a scene, so one asset can be referenced by every prefab and scene that needs it, and a change in one place shows up everywhere. Once that clicks, you start spotting uses for them all over a project.
One warning before the list, because it catches everyone at least once. In the Editor, any change a ScriptableObject picks up during Play mode is written back into the asset and is still there when you stop. In a build, the same changes disappear when the game restarts. Keep that in mind for every example below that holds state while the game runs.
Here are ten uses I keep coming back to, each with code you can drop into a project.
1. Storing game data
The classic use, and the place to start. Player stats, item definitions and level settings live in assets that designers can tweak without touching code or digging through prefabs.
using UnityEngine;
[CreateAssetMenu(fileName = "NewPlayerData", menuName = "Game/Player Data")]
public class PlayerData : ScriptableObject
{
public int health = 100;
public int strength = 10;
public int agility = 10;
public int intelligence = 10;
}
Any component that needs these values takes a PlayerData field, and you drag the asset in. If five enemy prefabs share one stats asset, one edit rebalances all five.
2. Game events
ScriptableObjects make good event channels: whatever raises the event and whatever reacts to it only know about the asset, never about each other. I go deeper on this kind of decoupling in my post on event-based architecture in Unity.
The first version of this example put a UnityEvent straight on the asset. It looks tidy, but an asset can’t store references to objects in a scene, so you can’t wire scene listeners to it in the Inspector. The version that works pairs the asset with a small listener component:
using System;
using UnityEngine;
[CreateAssetMenu(fileName = "NewGameEvent", menuName = "Game/Game Event")]
public class GameEvent : ScriptableObject
{
public event Action Raised;
public void Raise() => Raised?.Invoke();
}
using UnityEngine;
using UnityEngine.Events;
public class GameEventListener : MonoBehaviour
{
[SerializeField] private GameEvent gameEvent;
[SerializeField] private UnityEvent response;
private void OnEnable() => gameEvent.Raised += response.Invoke;
private void OnDisable() => gameEvent.Raised -= response.Invoke;
}
Put a GameEventListener on any object, point it at the event asset, and wire up the response in the Inspector the same way you would a button click. Anything holding a reference to the asset can call Raise().
3. Custom editor tools
ScriptableObjects pair well with custom inspectors, because both the data and the tool that edits it live outside any scene. Here’s a level list with a button that checks every scene is actually in Build Settings, a mistake that otherwise only shows up when a player taps “next level”.
// Assets/Scripts/LevelDatabase.cs
using UnityEngine;
[CreateAssetMenu(fileName = "LevelDatabase", menuName = "Game/Level Database")]
public class LevelDatabase : ScriptableObject
{
public string[] sceneNames;
}
// Assets/Scripts/Editor/LevelDatabaseEditor.cs
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(LevelDatabase))]
public class LevelDatabaseEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (GUILayout.Button("Check scenes are in Build Settings"))
{
var database = (LevelDatabase)target;
foreach (string sceneName in database.sceneNames)
{
bool inBuild = System.Array.Exists(
EditorBuildSettings.scenes,
s => s.enabled && s.path.EndsWith($"/{sceneName}.unity"));
if (!inBuild)
Debug.LogWarning($"{sceneName} is missing from Build Settings", database);
}
}
}
}
Mind the folder. Anything that uses UnityEditor has to live in a folder called Editor (or sit behind #if UNITY_EDITOR), otherwise your player builds fail to compile. The first version of this post put the editor class in a normal script, which is exactly that mistake.
4. Bundling related assets
A ScriptableObject can’t stand in for a material or a mesh, but it’s a great way to keep assets that belong together in one place. A character skin, for example, is a material, an icon and a sound that should always travel as a set:
using UnityEngine;
[CreateAssetMenu(fileName = "NewSkin", menuName = "Game/Character Skin")]
public class CharacterSkin : ScriptableObject
{
public string displayName;
public Material material;
public Sprite icon;
public AudioClip selectSound;
}
The shop, the character renderer and the menu all take a CharacterSkin, so adding a new skin means creating one asset instead of editing three systems.
5. Global settings
Settings that many systems read, like audio toggles or the current difficulty, fit nicely in one asset that any script can reference without a singleton in the scene:
using UnityEngine;
[CreateAssetMenu(fileName = "GameSettings", menuName = "Game/Game Settings")]
public class GameSettings : ScriptableObject
{
public bool soundEnabled = true;
public bool musicEnabled = true;
[Range(0, 2)] public int difficulty = 1;
}
This is where the Play mode warning bites hardest. Turn sound off while testing in the Editor and it stays off in the asset after you stop. In a build it resets on every launch. Load the player’s real choices from PlayerPrefs or a save file when the game starts, and treat the asset as the defaults.
6. Audio cues
Instead of every script holding its own AudioClip and volume, an audio cue asset describes how a sound should play: which clips, how loud, and how much the pitch varies. A little random variation is what stops a footstep from sounding like a machine gun.
using UnityEngine;
[CreateAssetMenu(fileName = "NewAudioCue", menuName = "Game/Audio Cue")]
public class AudioCue : ScriptableObject
{
public AudioClip[] clips;
[Range(0f, 1f)] public float volume = 1f;
public Vector2 pitchRange = new Vector2(0.95f, 1.05f);
public void Play(AudioSource source)
{
if (clips.Length == 0) return;
source.pitch = Random.Range(pitchRange.x, pitchRange.y);
source.PlayOneShot(clips[Random.Range(0, clips.Length)], volume);
}
}
The original version of this example called AudioSource.PlayClipAtPoint at the world origin. That spawns a 3D sound at position (0, 0, 0), so it gets quieter the further your camera is from the centre of the level. Passing in an AudioSource lets the caller decide where the sound comes from.
7. Object pool settings
Pooling saves you from instantiating and destroying the same objects over and over. The first version of this example kept the live pool inside the ScriptableObject, which breaks as soon as a scene unloads: the pooled objects are destroyed with the scene, but the asset still holds references to them. Keep only the pool’s settings in the asset, and let a component in the scene own the objects through the ObjectPool<T> class built into Unity since 2021:
using UnityEngine;
[CreateAssetMenu(fileName = "NewPoolSettings", menuName = "Game/Pool Settings")]
public class PoolSettings : ScriptableObject
{
public GameObject prefab;
public int defaultCapacity = 20;
public int maxSize = 100;
}
using UnityEngine;
using UnityEngine.Pool;
public class Spawner : MonoBehaviour
{
[SerializeField] private PoolSettings settings;
private ObjectPool<GameObject> pool;
private void Awake()
{
pool = new ObjectPool<GameObject>(
createFunc: () => Instantiate(settings.prefab),
actionOnGet: obj => obj.SetActive(true),
actionOnRelease: obj => obj.SetActive(false),
actionOnDestroy: obj => Destroy(obj),
defaultCapacity: settings.defaultCapacity,
maxSize: settings.maxSize);
}
public GameObject Spawn() => pool.Get();
public void Despawn(GameObject obj) => pool.Release(obj);
}
The asset still earns its place: designers can tune pool sizes per enemy type without touching the spawner.
8. Shared input state
An input asset gives every system one place to read input from, instead of each script calling Input.GetAxis itself. Something still has to update it every frame, so pair it with a single component that does that job and runs before everything else:
using UnityEngine;
[CreateAssetMenu(fileName = "PlayerInput", menuName = "Game/Player Input")]
public class PlayerInputState : ScriptableObject
{
public float horizontal;
public float vertical;
public bool jumpPressed;
}
using UnityEngine;
[DefaultExecutionOrder(-100)]
public class InputReader : MonoBehaviour
{
[SerializeField] private PlayerInputState input;
private void Update()
{
input.horizontal = Input.GetAxis("Horizontal");
input.vertical = Input.GetAxis("Vertical");
input.jumpPressed = Input.GetButtonDown("Jump");
}
}
This uses the old Input Manager to keep the example short. If your project is set to the newer Input System only (common in Unity 6), Input.GetAxis throws an error. In that case, have the asset wrap your input actions and expose C# events instead, and nothing needs to poll at all.
9. Dialogue
Conversations are content, and content belongs in assets that writers can edit without opening a scene. A simple version:
using UnityEngine;
[CreateAssetMenu(fileName = "NewDialogue", menuName = "Game/Dialogue")]
public class Dialogue : ScriptableObject
{
[System.Serializable]
public class Line
{
public string speaker;
[TextArea(2, 6)] public string text;
}
public Line[] lines;
}
A dialogue runner component steps through lines and shows each one in turn. From there it grows easily: add a portrait sprite to each line, or a Dialogue[] choices field for branching conversations.
10. Debugging tools
ScriptableObjects make handy debug logs because any system can write to the same asset, and you can watch it in the Inspector while the game runs.
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "DebugLog", menuName = "Game/Debug Log")]
public class DebugLog : ScriptableObject
{
[SerializeField] private List<string> entries = new List<string>();
public IReadOnlyList<string> Entries => entries;
public void Add(string message) => entries.Add($"[frame {Time.frameCount}] {message}");
public void Clear() => entries.Clear();
}
Select the asset during Play mode and the entries update live in the Inspector. Because they’re serialized, they’re still there after you stop, which is handy for reading them afterwards and annoying if you forget, so clear the log when your game boots.
Where to start
If you’re new to ScriptableObjects, start with game data and event channels. Those two give you most of the value, and they’ll show you quickly where the Play mode gotcha matters in your project. The rest are worth reaching for once a project is big enough that the alternative is another singleton or another hand-edited prefab.
If you’d rather have someone who has shipped sixteen Unity games set this up for your project, that’s the kind of work I do for client teams.
Common questions
What are ScriptableObjects used for in Unity?
ScriptableObjects are data containers saved as project assets rather than scene objects. Common uses include storing game data like player stats and items, defining game events, holding global settings, and driving systems like dialogue, object pooling, and audio. Because they live outside any scene, the same asset can be referenced everywhere.
How do I create a ScriptableObject asset in Unity?
Write a class that inherits from ScriptableObject and decorate it with the CreateAssetMenu attribute, giving it a fileName and menuName. That adds an entry to the Assets Create menu, so you can right-click in the Project window and create instances of it as assets.
Can ScriptableObjects be used for game events?
Yes. Give the ScriptableObject a C# event and a Raise method, then add a small listener component to scene objects that subscribes in OnEnable and unsubscribes in OnDisable. Senders and listeners only reference the event asset, never each other. A UnityEvent stored on the asset itself can't point at scene objects, which is why the listener component exists.
Are ScriptableObjects good for storing player data?
They're a good fit for design-time data like base stats, item definitions, and level settings. They are not a save system: changes made while the game runs are written back to the asset in the Editor but lost when a built game restarts. Save player progress to a file or a backend instead.
Can a ScriptableObject work as a global manager?
Yes, for settings-style state such as sound and music toggles or the current difficulty. Any script can reference the asset without needing a singleton MonoBehaviour in the scene. Treat the values as defaults, though, and load the player's saved choices into it when the game starts.