ScriptableObjects in Unity: A Practical Guide with 5 Patterns

Illustration of glowing data cards connected by light beams to game items

Written by

in

If your enemy prefabs each carry their own copy of health, speed and damage values, or you have a “GameManager” singleton that every script reaches into, ScriptableObjects can clean that up. A ScriptableObject is a data container that lives as an asset in your project instead of on a GameObject in a scene. This guide covers what they are, when to use them, and five practical patterns (data assets, item databases, shared variables, event channels and runtime sets). It also covers the one gotcha that confuses everyone: changes made in Play mode that stay after you stop.

What Is a ScriptableObject?

A MonoBehaviour is a component. It has to be attached to a GameObject in a scene or prefab, and every copy has its own values. A ScriptableObject is a class whose instances are saved as .asset files in your project. Many objects can reference the same asset, so the data exists once.

Three things follow from that:

  • Less duplication. 200 goblins can all point to one GoblinStats asset. Change the speed once and every goblin changes.
  • Less memory. Values stored on a prefab are copied into every instance. A reference to an asset is just a reference.
  • Looser coupling. Scripts can talk through shared assets instead of finding each other in the scene, so a UI prefab and a player prefab don’t need to know about each other.

ScriptableObjects don’t get Update, don’t have a Transform and can’t be attached to GameObjects. They do get OnEnable, OnDisable, OnDestroy and OnValidate.

Creating Your First One

using UnityEngine;

[CreateAssetMenu(fileName = "NewEnemyStats", menuName = "Game/Enemy Stats")]
public class EnemyStats : ScriptableObject
{
    public string displayName = "Goblin";
    public int maxHealth = 30;
    public float moveSpeed = 3.5f;
    public int damage = 5;
    public GameObject deathEffect;
}

The [CreateAssetMenu] attribute adds an entry to the Project window: right-click, then Create > Game > Enemy Stats. Make a few assets (Goblin, Orc, Skeleton) and fill in their values in the Inspector.

Then reference the asset from a MonoBehaviour like any other field:

public class Enemy : MonoBehaviour
{
    [SerializeField] EnemyStats stats;
    int health;

    void Awake() => health = stats.maxHealth;

    void Update()
    {
        transform.Translate(Vector3.forward * stats.moveSpeed * Time.deltaTime);
    }
}

Drag the Goblin asset into the prefab’s Stats slot. Every goblin now reads from the same asset.

To create one from code (for example in an editor tool), use ScriptableObject.CreateInstance<EnemyStats>(), never new EnemyStats(). Save it as an asset with AssetDatabase.CreateAsset in editor code.

Pattern 1: Shared Data Assets

This is the pattern above, and it’s the one to start with. Good candidates are anything a designer tunes and many objects share: enemy stats, weapon definitions, level settings, difficulty presets, audio settings, dialog lines.

A useful trick: put behavior-free logic that only depends on the data in the ScriptableObject too.

[CreateAssetMenu(menuName = "Game/Weapon")]
public class WeaponData : ScriptableObject
{
    public float baseDamage = 10f;
    public float critChance = 0.1f;
    public float critMultiplier = 2f;

    public float RollDamage() =>
        Random.value < critChance ? baseDamage * critMultiplier : baseDamage;
}

Swapping weapons is now just swapping which asset the player holds.

Pattern 2: An Item Database

For inventories, give every item its own asset, and keep one database asset that lists them. Save files then store only an item ID, never the asset itself.

[CreateAssetMenu(menuName = "Game/Item")]
public class ItemData : ScriptableObject
{
    public string id;           // stable ID for save files, e.g. "potion_small"
    public string displayName;
    public Sprite icon;
    public int maxStack = 99;
}

[CreateAssetMenu(menuName = "Game/Item Database")]
public class ItemDatabase : ScriptableObject
{
    public ItemData[] items;
    Dictionary<string, ItemData> lookup;

    public ItemData Get(string id)
    {
        lookup ??= items.ToDictionary(i => i.id);
        return lookup.TryGetValue(id, out var item) ? item : null;
    }
}

(This needs using System.Collections.Generic; and using System.Linq;.) When loading a save, turn each stored ID back into an item with database.Get(id). See our JSON save system guide for the saving side.

Pattern 3: Shared Variables

Instead of the health bar finding the player to read its health, both reference a FloatVariable asset. The player writes it, the UI reads it, and neither knows the other exists.

[CreateAssetMenu(menuName = "Variables/Float")]
public class FloatVariable : ScriptableObject
{
    public float initialValue;
    [System.NonSerialized] public float Value;

    void OnEnable() => Value = initialValue;
}

[NonSerialized] keeps the runtime value from being saved into the asset, and OnEnable resets it. Without this, values changed during Play mode stay in the asset in the Editor (see below).

Use this pattern in moderation. A handful of shared variables (player health, score, currency) is clean. Hundreds of them make it hard to follow who writes what.

Pattern 4: Event Channels

An event channel is a ScriptableObject that other objects can raise and listen to. It’s a great way to connect things across scenes and prefabs, like “player died” triggering the game-over screen, music and analytics.

[CreateAssetMenu(menuName = "Events/Void Event Channel")]
public class VoidEventChannel : ScriptableObject
{
    public event System.Action OnRaised;
    public void Raise() => OnRaised?.Invoke();
}

// Raiser
public class PlayerHealth : MonoBehaviour
{
    [SerializeField] VoidEventChannel playerDied;
    public void Die() => playerDied.Raise();
}

// Listener
public class GameOverScreen : MonoBehaviour
{
    [SerializeField] VoidEventChannel playerDied;
    [SerializeField] GameObject panel;

    void OnEnable()  => playerDied.OnRaised += Show;
    void OnDisable() => playerDied.OnRaised -= Show;
    void Show() => panel.SetActive(true);
}

Always unsubscribe in OnDisable. The asset outlives scenes, so a destroyed listener that never unsubscribed will still be called and throw a MissingReferenceException.

📚 Want to go deeper on game architecture?

ScriptableObject patterns are one piece of a bigger topic: structuring game code that stays easy to change. Our list of the best programming and game design books includes the ones we’d recommend for that.

See the book recommendations →

Pattern 5: Runtime Sets

A runtime set is a list asset that objects add themselves to while they’re active. Other systems read the list instead of calling FindObjectsByType.

[CreateAssetMenu(menuName = "Sets/Enemy Set")]
public class EnemySet : ScriptableObject
{
    [System.NonSerialized] public List<Enemy> Items = new();
    public void Add(Enemy e)    { if (!Items.Contains(e)) Items.Add(e); }
    public void Remove(Enemy e) => Items.Remove(e);
}

public class Enemy : MonoBehaviour
{
    [SerializeField] EnemySet allEnemies;
    void OnEnable()  => allEnemies.Add(this);
    void OnDisable() => allEnemies.Remove(this);
}

A minimap, a wave spawner and an “enemies remaining” counter can all read allEnemies.Items with no searching.

Play Mode Changes and Saving

This is the most important thing to understand about ScriptableObjects:

  • In the Editor, a ScriptableObject asset is the real asset. If your code changes a serialized field during Play mode, the change stays after you stop playing, and it gets saved to disk. Your “max health 30” goblin might be “max health 12” next time.
  • In a build, assets are read-only. Changes last until the asset is unloaded or the game closes, then they’re gone. ScriptableObjects are not a save system.

Rules that avoid both problems:

  1. Treat design data (stats, item definitions) as read-only at runtime. Copy values into the MonoBehaviour (health = stats.maxHealth) and change the copy.
  2. For runtime state that must live in an asset (shared variables, runtime sets), mark it [NonSerialized] and reset it in OnEnable.
  3. For progress that must survive quitting, write it to a save file.

Note that OnEnable on a ScriptableObject doesn’t necessarily run every time you enter Play mode if Enter Play Mode Options has domain reload disabled. In that case, reset values from a bootstrap script, or subscribe to EditorApplication.playModeStateChanged in editor code.

Troubleshooting

“The asset menu item doesn’t appear”

Check the script compiles, the class derives from ScriptableObject, the file name matches the class name, and [CreateAssetMenu] is on the class. The menu is under right-click Create in the Project window.

Values reset or change unexpectedly

Runtime code is writing to a serialized field on a design asset. Search your code for writes to the asset’s fields and move that state into a MonoBehaviour or a [NonSerialized] field.

“ScriptableObject should be created with CreateInstance”

You used new on a ScriptableObject class. Use ScriptableObject.CreateInstance<T>().

Changes made from an editor script disappear on restart

Changing an asset from code doesn’t mark it dirty. Call EditorUtility.SetDirty(asset) and then AssetDatabase.SaveAssets().

MissingReferenceException from an event channel

A destroyed listener is still subscribed. Subscribe in OnEnable and unsubscribe in OnDisable.

Asset reference is None in a build

Assets are only included in a build when something in a built scene references them (or they’re in a Resources folder or Addressables). Make sure a scene object holds the reference.

FAQ

What is a ScriptableObject used for in Unity?

Storing shared data as project assets: enemy stats, item definitions, settings and event channels. Many objects can reference one asset, which removes duplication and coupling.

Do ScriptableObject changes save at runtime?

In the Editor, changes made in Play mode are kept in the asset. In builds they’re lost when the game closes. Use a save file for persistent progress.

ScriptableObject vs MonoBehaviour: what’s the difference?

A MonoBehaviour is a component on a GameObject with Update and a Transform. A ScriptableObject is a standalone asset for data with no GameObject, Transform or Update.

Can a ScriptableObject have an Update method?

No. Unity doesn’t call Update on ScriptableObjects. Drive per-frame logic from a MonoBehaviour that uses the asset.

Are ScriptableObjects better than singletons?

For shared data and events, often yes: they’re easier to test and don’t need scene lookups. For systems that need Update or coroutines, a MonoBehaviour manager is still simpler.

How do I create a ScriptableObject from code?

Use ScriptableObject.CreateInstance<T>(). To save it as an asset in the Editor, call AssetDatabase.CreateAsset with a path ending in .asset.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *