ScriptableObjects: The Complete Guide

Learn how to use ScriptableObjects for data-driven design, configuration, and decoupled architecture.

ScriptableObjects are one of Unity's most powerful features, yet many developers underuse them. They're not just data containers — they can serve as event channels, runtime sets, and even lightweight state machines. Once you understand the pattern, you'll find yourself reaching for ScriptableObjects in almost every project.

This guide covers everything from the basics to advanced patterns. By the end, you'll know how to use ScriptableObjects for game configuration, item databases, event-driven architecture, and more. We'll also compare them to MonoBehaviours so you know exactly when to use each. If you're building an inventory system or a dialogue system, ScriptableObjects will be the backbone of your data architecture.

What Are ScriptableObjects?

A ScriptableObject is a data container that lives as an asset in your project's Assets folder. Unlike MonoBehaviours, ScriptableObjects don't need to be attached to GameObjects and aren't tied to any particular scene. They're serialized by Unity's asset pipeline, which means you can edit them in the Inspector, reference them from any scene, and version-control them as individual .asset files.

Under the hood, a ScriptableObject is a Unity Object — it has an instance ID, it participates in garbage collection, and it can be loaded/unloaded by the asset system. In the Editor, ScriptableObjects persist between play sessions (changes made in Play Mode stick). In a build, they're read-only assets baked into the player — though you can modify their runtime values in memory, those changes won't be saved to disk.

Creating a ScriptableObject

Creating a ScriptableObject takes two steps: define the class, then create instances as assets. The [CreateAssetMenu] attribute adds a menu item to the Project window's right-click menu, so designers can create new assets without writing code.

C#
using UnityEngine;

[CreateAssetMenu(fileName = "NewWeapon", menuName = "Game/Weapon Data", order = 1)]
public class WeaponData : ScriptableObject
{
    [Header("Identity")]
    public string weaponName;
    public Sprite icon;
    [TextArea] public string description;

    [Header("Stats")]
    public int damage = 10;
    public float attackSpeed = 1f;
    public float range = 2f;

    [Header("Effects")]
    public AudioClip attackSound;
    public GameObject hitVFX;
    public AnimationClip attackAnimation;
}

To create an asset: right-click in the Project window → Create → Game → Weapon Data. Unity creates a .asset file that you can rename, configure in the Inspector, and reference from any MonoBehaviour in any scene. You can create dozens of weapon variants — sword, axe, bow, staff — all from the same class, each with different stats.

Use Case 1: Game Configuration

Centralized Settings Without Hard-Coded Values

Instead of scattering magic numbers across your scripts, define a GameConfig ScriptableObject that holds all your tunable values. Designers can tweak gravity, spawn rates, difficulty curves, and economy settings directly in the Inspector — no code changes, no recompilation, no merge conflicts.

This pattern is especially powerful for balancing. Create multiple config assets — EasyConfig, NormalConfig, HardConfig — and swap them at runtime by changing a single reference. Your game manager doesn't care which config it's holding; it just reads config.enemySpawnRate.

C#
using UnityEngine;

[CreateAssetMenu(menuName = "Game/Game Config")]
public class GameConfig : ScriptableObject
{
    [Header("Player")]
    public float playerSpeed = 5f;
    public int startingHealth = 100;
    public float invincibilityDuration = 1.5f;

    [Header("Enemies")]
    public float enemySpawnRate = 2f;
    public int maxEnemies = 20;
    public AnimationCurve difficultyOverTime;

    [Header("Economy")]
    public int startingGold = 50;
    public float shopPriceMultiplier = 1f;
}

// Usage in a MonoBehaviour
public class GameManager : MonoBehaviour
{
    [SerializeField] private GameConfig config;

    private void Start()
    {
        playerController.SetSpeed(config.playerSpeed);
        healthSystem.SetMaxHealth(config.startingHealth);
        spawner.SetRate(config.enemySpawnRate);
    }
}

Use Case 2: Item Databases

Data-Driven Inventory and Loot Tables

ScriptableObjects are the perfect foundation for item databases. Each item type (weapon, armor, consumable) gets its own ScriptableObject class, and each variant (Iron Sword, Fire Staff, Health Potion) is an asset in the Project. Your inventory system stores references to these assets rather than duplicating data — 50 Iron Swords in 50 different inventories all point to the same IronSword.asset.

For loot tables, create a ScriptableObject that holds an array of item references with drop weights. The loot system picks a random item based on weight, instantiates the drop, and assigns the ScriptableObject reference. Designers can build and balance loot tables entirely in the Inspector.

C#
using UnityEngine;

[CreateAssetMenu(menuName = "Game/Item")]
public class ItemData : ScriptableObject
{
    public string itemName;
    public Sprite icon;
    [TextArea] public string description;
    public ItemRarity rarity;
    public int maxStack = 1;
    public int buyPrice;
    public int sellPrice;
}

public enum ItemRarity { Common, Uncommon, Rare, Epic, Legendary }

[CreateAssetMenu(menuName = "Game/Loot Table")]
public class LootTable : ScriptableObject
{
    [System.Serializable]
    public struct LootEntry
    {
        public ItemData item;
        [Range(0f, 100f)] public float dropChance;
        public int minQuantity;
        public int maxQuantity;
    }

    public LootEntry[] entries;

    public ItemData Roll()
    {
        float roll = Random.Range(0f, 100f);
        float cumulative = 0f;

        foreach (var entry in entries)
        {
            cumulative += entry.dropChance;
            if (roll <= cumulative)
                return entry.item;
        }

        return null; // No drop
    }
}

Use Case 3: Event Channels

Decoupled Communication via ScriptableObject Events

One of the most powerful ScriptableObject patterns is the event channel. Instead of using static C# events (which create compile-time coupling) or a Singleton event bus (which creates a single point of failure), you create a ScriptableObject that acts as a named event. Publishers raise the event; subscribers listen. Neither side knows the other exists — they only know about the shared ScriptableObject asset.

This pattern, popularized by Ryan Hipple's Unite 2017 talk, makes your systems truly modular. The health system raises OnPlayerDied (a ScriptableObject asset). The UI, the audio system, and the achievement tracker each reference that same asset and subscribe. You can rewire the entire event graph in the Inspector without editing a single line of code.

C#
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
    private readonly List<GameEventListener> listeners = new();

    public void Raise()
    {
        // Iterate backwards in case a listener unregisters during the call
        for (int i = listeners.Count - 1; i >= 0; i--)
            listeners[i].OnEventRaised();
    }

    public void Register(GameEventListener listener) => listeners.Add(listener);
    public void Unregister(GameEventListener listener) => listeners.Remove(listener);
}

public class GameEventListener : MonoBehaviour
{
    [SerializeField] private GameEvent gameEvent;
    [SerializeField] private UnityEngine.Events.UnityEvent response;

    private void OnEnable() => gameEvent.Register(this);
    private void OnDisable() => gameEvent.Unregister(this);

    public void OnEventRaised() => response.Invoke();
}

// Publisher — knows nothing about subscribers
public class HealthSystem : MonoBehaviour
{
    [SerializeField] private GameEvent onPlayerDied;

    public void TakeDamage(int amount)
    {
        currentHealth -= amount;
        if (currentHealth <= 0)
            onPlayerDied.Raise();
    }
}

Drag the same OnPlayerDied.asset into the health system's publisher field and every listener's subscriber field. The Inspector becomes your event wiring diagram — no code required to connect systems.

Use Case 4: Runtime Sets

Track Active Objects Without FindObjectsOfType

A runtime set is a ScriptableObject that maintains a list of active objects. Each object registers itself in OnEnable and unregisters in OnDisable. Any system that needs to iterate over all enemies, all pickups, or all interactable NPCs simply references the runtime set asset — no expensive FindObjectsOfType calls, no Singleton manager required.

This pattern scales beautifully. Your minimap system references AllEnemies runtime set to draw dots. Your AOE spell references the same set to deal damage to nearby enemies. Your kill counter references it to display the count. None of these systems know about each other — they all just read from the shared set.

C#
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Sets/Runtime Set")]
public class RuntimeSet : ScriptableObject
{
    private readonly List<GameObject> items = new();

    public IReadOnlyList<GameObject> Items => items;
    public int Count => items.Count;

    public void Add(GameObject obj)
    {
        if (!items.Contains(obj))
            items.Add(obj);
    }

    public void Remove(GameObject obj) => items.Remove(obj);

    // Clear on play mode exit to avoid stale references
    private void OnDisable() => items.Clear();
}

// Self-registering component — attach to any enemy prefab
public class RuntimeSetMember : MonoBehaviour
{
    [SerializeField] private RuntimeSet targetSet;

    private void OnEnable() => targetSet.Add(gameObject);
    private void OnDisable() => targetSet.Remove(gameObject);
}

// Consumer — minimap reads the set each frame
public class Minimap : MonoBehaviour
{
    [SerializeField] private RuntimeSet enemies;

    private void Update()
    {
        foreach (var enemy in enemies.Items)
            DrawDot(enemy.transform.position);
    }

    private void DrawDot(Vector3 worldPos) { /* ... */ }
}

Use Case 5: Variable References

Shared Variables Without Singletons

A variable reference is a ScriptableObject that wraps a single value — a float, an int, a string. Systems that need to read or write the player's health, the current score, or the time remaining all reference the same FloatVariable.asset. This eliminates the need for a Singleton to hold shared state and makes the data flow visible in the Inspector.

The beauty of this pattern is that the writer doesn't know who's reading, and the readers don't know who's writing. The health bar reads PlayerHealth.asset. The damage system writes to it. The audio system checks it to decide whether to play a low-health heartbeat. Each system is completely independent — you can delete the health bar and nothing else breaks.

C#
using UnityEngine;

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

    private void OnEnable()
    {
        runtimeValue = initialValue;
    }
}

[CreateAssetMenu(menuName = "Variables/Int Variable")]
public class IntVariable : ScriptableObject
{
    public int initialValue;
    [System.NonSerialized] public int runtimeValue;

    private void OnEnable()
    {
        runtimeValue = initialValue;
    }
}

// Writer — health system
public class HealthSystem : MonoBehaviour
{
    [SerializeField] private FloatVariable playerHealth;

    public void TakeDamage(float amount)
    {
        playerHealth.runtimeValue -= amount;
        playerHealth.runtimeValue = Mathf.Max(0, playerHealth.runtimeValue);
    }
}

// Reader — health bar UI (completely decoupled from HealthSystem)
public class HealthBar : MonoBehaviour
{
    [SerializeField] private FloatVariable playerHealth;
    [SerializeField] private UnityEngine.UI.Slider slider;

    private void Update()
    {
        slider.value = playerHealth.runtimeValue / playerHealth.initialValue;
    }
}

ScriptableObject vs MonoBehaviour

Choosing between ScriptableObject and MonoBehaviour is one of the most common architectural decisions in Unity. Here's a direct comparison to help you decide:

The rule of thumb: if it's data (stats, config, items, events), use a ScriptableObject. If it's behavior (movement, rendering, physics interaction), use a MonoBehaviour. Many systems use both: a MonoBehaviour that holds a reference to a ScriptableObject for its data.

Advanced: Addressable ScriptableObjects

For large projects with hundreds of ScriptableObject assets (think: an RPG with 500 items), loading everything at startup is wasteful. Unity's Addressables system lets you mark ScriptableObjects as addressable assets and load them on demand. Instead of a direct reference (which forces the asset into memory at scene load), you hold an AssetReference and load the ScriptableObject asynchronously when needed.

This is particularly valuable for mobile games where memory is tight. Your loot table can reference item IDs or addressable keys instead of direct ScriptableObject references. When the player opens a chest, you load only the dropped item's data. When they close the inventory panel, you release it. This pattern reduces your base memory footprint significantly.

C#
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class AddressableItemLoader : MonoBehaviour
{
    [SerializeField] private AssetReference itemReference;

    private ItemData loadedItem;

    public async void LoadItem()
    {
        AsyncOperationHandle<ItemData> handle = 
            itemReference.LoadAssetAsync<ItemData>();

        loadedItem = await handle.Task;
        Debug.Log($"Loaded item: {loadedItem.itemName}");
    }

    public void UnloadItem()
    {
        if (loadedItem != null)
        {
            itemReference.ReleaseAsset();
            loadedItem = null;
        }
    }
}

Advanced: Custom Editor Tooling

ScriptableObjects shine when paired with custom Editor scripts. You can build Inspector tools that let designers create, search, and batch-edit ScriptableObject assets without leaving the Unity Editor. A custom PropertyDrawer can render item icons inline, a custom Editor can add validation buttons, and an EditorWindow can provide a database browser for hundreds of assets.

For example, an item database editor window might display a searchable grid of all ItemData assets in the project, with filters for rarity, type, and price range. Clicking an item opens its Inspector. A "Validate All" button checks every item for missing icons, zero-value stats, or duplicate names. This kind of tooling dramatically improves the designer workflow and catches errors before they reach the build.

C#
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(ItemData))]
public class ItemDataEditor : Editor
{
    public override void OnInspectorGUI()
    {
        ItemData item = (ItemData)target;

        // Draw icon preview at the top
        if (item.icon != null)
        {
            GUILayout.BeginHorizontal();
            GUILayout.FlexibleSpace();
            Rect rect = GUILayoutUtility.GetRect(64, 64);
            EditorGUI.DrawPreviewTexture(rect, item.icon.texture);
            GUILayout.FlexibleSpace();
            GUILayout.EndHorizontal();
        }

        DrawDefaultInspector();

        EditorGUILayout.Space();

        // Validation button
        if (GUILayout.Button("Validate Item"))
        {
            bool valid = true;
            if (string.IsNullOrEmpty(item.itemName))
            { Debug.LogWarning($"{item.name}: Missing item name!"); valid = false; }
            if (item.icon == null)
            { Debug.LogWarning($"{item.name}: Missing icon!"); valid = false; }
            if (item.buyPrice <= 0)
            { Debug.LogWarning($"{item.name}: Buy price is zero!"); valid = false; }

            if (valid)
                Debug.Log($"{item.itemName} passed validation.");
        }
    }
}
#endif

ScriptableObjects are the backbone of data-driven Unity development. Start with simple data containers (game config, item stats), graduate to event channels and runtime sets as your project grows, and layer in Addressables and editor tooling when scale demands it. Every system in our Inventory System and Dialogue System bundles uses ScriptableObjects as the primary data architecture.