Sunday, 28 June 2026

How to Use Unity ScriptableObjects: Fix Messy Data and Stop Hardcoding Values

Here is a problem almost every Unity developer hits at a specific point in their project. You have a player with a movement speed of 6, an enemy with a patrol speed of 3.5, a weapon dealing 25 damage, and a health system starting at 100 — and every single one of those numbers is buried inside a different script, scattered across a dozen MonoBehaviours, duplicated in two or three places, and nearly impossible to tweak without hunting through files one by one.

I ran into this exact wall while building the NPC state machine for a survival game project. My enemy patrol speed was hardcoded in one script, its detection radius in another, and its damage value somewhere else entirely. Changing anything meant opening three files, hoping I had found every instance, and testing whether the change broke something I had not touched. It was slow, error-prone, and frankly embarrassing for code I was supposed to be proud of.

ScriptableObjects solve this problem. They are Unity's built-in system for storing data as standalone assets — separate from any scene or GameObject — that any script in your project can read from without being tightly coupled to each other. This guide explains exactly how they work, how to create them, and how to rebuild the most common beginner data patterns using them properly.



What Is a ScriptableObject in Unity?

A ScriptableObject is a Unity class you can inherit from (instead of MonoBehaviour) to create data containers that live as standalone asset files in your project rather than as components attached to a GameObject. Unlike a MonoBehaviour, a ScriptableObject has no Update() loop, no Transform, and no scene presence — it is just data, organized in a file, that can be referenced from anywhere.

You create a ScriptableObject the same way you create a material or an animation clip — it is an asset in your Project window that persists between play sessions, survives scene loads, and can be shared between multiple scripts and GameObjects simultaneously without any of them needing to know about each other.

Why ScriptableObjects Matter

The moment your project has more than a handful of scripts, hardcoded values become a maintenance problem. Change a value in one script, forget it is duplicated in another, and you have a bug that took two minutes to introduce and two hours to find.

ScriptableObjects solve this by moving shared data out of scripts entirely and into a single authoritative asset. Every script that needs to know "how fast does this enemy move?" reads from one EnemyData ScriptableObject. You change the speed once, in one place, and every enemy in every scene that references that asset updates automatically — no script editing, no recompilation, no hunting.

Beyond data management, ScriptableObjects are also a foundation for some genuinely elegant architecture patterns, including decoupled event systems and runtime item databases, which we will cover in the advanced examples section later in this guide.



ScriptableObject vs MonoBehaviour

Feature MonoBehaviour ScriptableObject
Lives on A GameObject in a scene An asset file in the Project window
Has Update() / Start() Yes No — it is data only, not a runtime loop
Persists between scenes Only with DontDestroyOnLoad() Yes, always — it is a project asset
Can be shared between multiple objects Not cleanly — each script has its own values Yes — any number of GameObjects can reference the same asset
Editable in the Inspector Yes, per-component Yes, as a standalone asset
Requires a GameObject to exist Yes No

When To Use ScriptableObjects

  • Any value that multiple scripts or multiple instances need to share — enemy stats, weapon damage, movement speeds.
  • Data that a designer or non-programmer needs to tweak regularly in the Inspector without opening any scripts.
  • Item databases, character classes, and other configuration sets where you want multiple variants (EnemySoldierData, EnemyBossData) sharing the same structure but different values.
  • Decoupled event systems where scripts need to communicate without knowing about each other directly (an advanced pattern covered later in this guide).
  • Runtime sets — tracking which objects currently exist in the scene without a manager singleton knowing about every object explicitly.

When Not To Use ScriptableObjects

  • For data that changes at runtime per-instance. A ScriptableObject's values are shared — if EnemyData stores currentHealth, every enemy in the scene shares the same currentHealth field, which is almost never what you want. Treat ScriptableObjects as read-only configuration, and store per-instance runtime state in the MonoBehaviour scripts on each enemy.
  • For simple, truly project-global constants that will genuinely never change — a static class or constants file can be simpler for values like physics layer indices or tag string constants.
  • For data that needs to be serialized as per-player save data. ScriptableObjects work well as configuration templates, but for actual save data you need a different serialization approach (JSON, binary, or Unity's PlayerPrefs system).

This part usually confuses new users, and honestly it confused me too at first: a ScriptableObject is a configuration template, not a per-instance data store. The distinction matters because Unity shares the same ScriptableObject asset across everything that references it, in both the Editor and in builds. Mutating its values at runtime works in the Editor but can produce unexpected shared-state bugs in builds if you have not planned for it.



Project Setup

Recommended Folder Structure

  • Assets/Scripts/Data — your ScriptableObject class definitions.
  • Assets/Data — the actual ScriptableObject asset instances (EnemySoldierData.asset, PlayerConfig.asset, and so on).
  • Assets/Scripts — your MonoBehaviour scripts that reference those assets.

Separating the ScriptableObject definitions (the C# class files) from the ScriptableObject instances (the .asset files) makes your Project window much easier to navigate as the project grows, and makes it immediately clear at a glance which files are code and which are data.

Creating Your First ScriptableObject: Enemy Configuration

Step 1: Write the ScriptableObject Class

Create a new C# script in Assets/Scripts/Data and name it EnemyData. Replace all contents with the following.

using UnityEngine;

[CreateAssetMenu(fileName = "NewEnemyData", menuName = "Game Data/Enemy Data")]
public class EnemyData : ScriptableObject
{
    [Header("Movement")]
    public float moveSpeed = 3.5f;
    public float patrolRadius = 10f;

    [Header("Detection")]
    public float detectionRadius = 8f;
    public float chaseSpeed = 6f;

    [Header("Combat")]
    public float attackDamage = 15f;
    public float attackRange = 1.5f;
    public float attackCooldown = 1.2f;

    [Header("Health")]
    public float maxHealth = 100f;
}

Step 2: Create an Asset Instance

  1. Right-click in your Assets/Data folder inside the Project window.
  2. Choose Create > Game Data > Enemy Data — the exact menu path you defined in the CreateAssetMenu attribute above.
  3. Name the resulting file EnemySoldierData.
  4. Click it to select it, and you will see all the fields from your class appear in the Inspector, ready to edit.

You can create as many instances as you need. An EnemySoldierData with low stats and a large patrol radius, an EnemyBossData with high stats and short patrol range — both from the exact same class definition, but configured independently in the Inspector.

Step 3: Reference the Asset in a MonoBehaviour

using UnityEngine;
using UnityEngine.AI;

public class EnemyController : MonoBehaviour
{
    public EnemyData data;

    private NavMeshAgent agent;
    private float currentHealth;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.speed = data.moveSpeed;
        currentHealth = data.maxHealth;
    }

    public void TakeDamage(float amount)
    {
        currentHealth -= amount;

        if (currentHealth <= 0f)
        {
            Die();
        }
    }

    void Die()
    {
        Debug.Log(gameObject.name + " has died.");
        Destroy(gameObject);
    }
}

In the Inspector, drag your EnemySoldierData asset into the Data field on the EnemyController component. Now your enemy reads all its configuration from the ScriptableObject, and changing the patrol speed or detection radius only requires editing the asset — no script editing, no recompile.

Line By Line Explanation

[CreateAssetMenu(...)] is an attribute that adds your ScriptableObject to the right-click Create menu in the Project window. Without it, creating instances requires going through Assets > Create, which is harder to find. The menuName field controls exactly where in that menu the item appears.

public class EnemyData : ScriptableObject is the key difference from a regular MonoBehaviour script — inheriting from ScriptableObject instead of MonoBehaviour is the only thing that makes this a data container rather than a component.

[Header("Movement")] organizes the Inspector display into labeled sections. With several fields, this makes a big difference to readability in the Inspector, especially if a designer (or future you) is editing these values regularly.

public EnemyData data; in the MonoBehaviour is just a regular public reference field. Unity's Inspector lets you drag any asset of the right type into this slot, including your ScriptableObject assets.

agent.speed = data.moveSpeed; reads a value from the ScriptableObject and applies it to the NavMeshAgent at startup. This is the core workflow: ScriptableObject stores configuration, MonoBehaviour reads it and uses it for actual runtime behavior.


Real Example: Building a Weapon Item Database

Let's look at a more complete example — an item system where each weapon type is a ScriptableObject asset, and your inventory or loot system just keeps a list of these assets rather than hardcoding weapon stats anywhere.

The WeaponData ScriptableObject

using UnityEngine;

[CreateAssetMenu(fileName = "NewWeapon", menuName = "Game Data/Weapon")]
public class WeaponData : ScriptableObject
{
    public string weaponName;
    public Sprite icon;
    public float damage;
    public float fireRate;
    public float range;
    public int ammoCapacity;

    [TextArea(2, 4)]
    public string description;
}

A Simple Inventory That Uses It

using UnityEngine;
using System.Collections.Generic;

public class PlayerInventory : MonoBehaviour
{
    public List<WeaponData> availableWeapons;
    private WeaponData equippedWeapon;

    void Start()
    {
        if (availableWeapons.Count > 0)
        {
            EquipWeapon(availableWeapons[0]);
        }
    }

    public void EquipWeapon(WeaponData weapon)
    {
        equippedWeapon = weapon;
        Debug.Log("Equipped: " + equippedWeapon.weaponName +
                  " | Damage: " + equippedWeapon.damage);
    }
}

In the Inspector, you drag your weapon ScriptableObject assets directly into the Available Weapons list. Sword, assault rifle, sniper — each one is a separate asset file with its own configured values. Your PlayerInventory script itself has zero hardcoded weapon data; it only knows how to hold and equip weapons, not what any specific weapon's stats are. This separation is the core benefit of data-driven design.

When I refactored my survival game to use this pattern instead of hardcoded weapon values, the first thing I noticed was how much easier playtesting became. Adjusting weapon balance no longer meant opening a script, changing a constant, waiting for recompilation, entering play mode, and testing again. It meant opening the WeaponData asset in the Inspector, changing the damage field directly, entering play mode, and testing. No compile step at all for balance changes.

Advanced Pattern: ScriptableObject Game Events

This is one of the most powerful and underused patterns in Unity development. Instead of scripts calling functions on each other directly (tight coupling), you create a ScriptableObject event that acts as a communication channel — scripts subscribe to it or raise it without needing to know who else is listening.

The GameEvent ScriptableObject

using UnityEngine;
using System.Collections.Generic;

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

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

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

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

The GameEventListener MonoBehaviour

using UnityEngine;
using UnityEngine.Events;

public class GameEventListener : MonoBehaviour
{
    public GameEvent gameEvent;
    public UnityEvent response;

    void OnEnable()
    {
        gameEvent.RegisterListener(this);
    }

    void OnDisable()
    {
        gameEvent.UnregisterListener(this);
    }

    public void OnEventRaised()
    {
        response.Invoke();
    }
}

Using It in Practice

Create a GameEvent asset named OnPlayerDied. Your health script raises it when health reaches zero. Your UI script listens for it to show the death screen. Your respawn system listens for it to start a countdown. Your achievement system listens for it to check a death-related challenge.

None of those systems need to know about each other. None of them import or reference each other's scripts. They all just reference the same OnPlayerDied ScriptableObject asset. This is the kind of clean, decoupled architecture that makes larger projects manageable rather than a tangled mess of inter-script dependencies.



Connecting ScriptableObjects to the Rest of the Series

If you have followed this series from the Character Controller and Rigidbody guides through to the NavMesh and Animator articles, you already have code that would benefit directly from the patterns in this guide.

Your EnemyController (from the NavMesh guide) hardcodes patrol speed and detection radius as public floats directly on the component. Replacing those with a reference to an EnemyData ScriptableObject gives you the ability to swap one enemy type for another just by assigning a different asset in the Inspector — without touching the script at all.

Your HealthBarUI (from the Canvas guide) has maxHealth hardcoded as a float. Replacing it with a reference to a PlayerConfig ScriptableObject means your UI and your player movement script can both read from the same authoritative source for player stats, rather than each maintaining their own separate copy of the same value.

Your AnimatorBridge (from the Animator guide) has moveSpeed as a serialized field. Moving it into a PlayerConfig ScriptableObject means a designer can adjust player speed without hunting for which of several scripts contains the relevant field.

Troubleshooting Guide

Problem: My ScriptableObject does not appear in the Create menu.

Likely cause: The CreateAssetMenu attribute is missing from the class definition, or the class name does not exactly match the file name.

Fix: Confirm the attribute is on the line immediately above the class declaration, and that the script file is named exactly the same as the class (EnemyData.cs for public class EnemyData).

Problem: Changes I made during play mode persisted after I exited play mode.

Likely cause: You modified a ScriptableObject's fields at runtime. Unlike MonoBehaviour component values, ScriptableObject asset modifications during play mode affect the actual asset file.

Fix: Either avoid writing to ScriptableObject fields at runtime for values that should reset between sessions, or make a copy of the relevant data into a local variable at Start() and modify that copy instead.

Problem: All my enemies share the same health value and die at the same time.

Likely cause: Current health is stored directly in the ScriptableObject rather than in each enemy's own MonoBehaviour.

Fix: Move currentHealth to the MonoBehaviour script on each enemy, initialized from data.maxHealth at Start(). The ScriptableObject stores the starting configuration; the MonoBehaviour stores the live runtime state.

Problem: My ScriptableObject reference shows as "None (EnemyData)" in the Inspector even after assigning it.

Likely cause: The field type in the script does not match the ScriptableObject type you created, or the asset was created from the wrong class.

Fix: Confirm the public field type in the MonoBehaviour exactly matches the ScriptableObject class name, and that the asset file in the Project window was created from that specific class.

Problem: I cannot find my ScriptableObject asset after creating it.

Likely cause: It was created in a random folder because you right-clicked in the wrong location in the Project window.

Fix: Use the Project window's search bar and search for the asset name, then move it to your designated Assets/Data folder once found. This is exactly why setting up a dedicated data folder before creating assets matters.




Frequently Asked Questions

What is a ScriptableObject in Unity?

A ScriptableObject is a Unity class you can inherit from to create data containers that live as standalone asset files in your project, rather than as components on a GameObject. Multiple scripts can reference the same ScriptableObject without being coupled to each other.

What is the difference between ScriptableObject and MonoBehaviour?

MonoBehaviour lives on a GameObject in a scene, has a runtime Update loop, and is tied to the scene's lifecycle. ScriptableObject lives as a project asset, has no Update loop, and persists independently of any scene.

How do I create a ScriptableObject in Unity?

Write a class that inherits from ScriptableObject instead of MonoBehaviour, add the CreateAssetMenu attribute, then right-click in the Project window and choose Create followed by the path you defined in that attribute.

Can ScriptableObjects be shared between multiple GameObjects?

Yes — this is one of the core benefits. Any number of GameObjects can hold a reference to the same ScriptableObject asset, all reading from one authoritative source rather than maintaining separate copies of the same values.

Why should I use ScriptableObjects instead of hardcoded values?

Hardcoded values buried in scripts require opening and editing script files every time a value needs to change. ScriptableObject assets can be edited directly in the Inspector, without opening any script, and a single change propagates to everything that references that asset.

Can I store runtime data like currentHealth in a ScriptableObject?

Technically yes, but this is almost always a mistake — all objects sharing that ScriptableObject will share the same currentHealth value. Store configuration in ScriptableObjects and runtime state in MonoBehaviour components.

Do ScriptableObject changes persist between play sessions?

Yes, including changes made during play mode in the Editor, which is different from MonoBehaviour component values that reset when you exit play mode. Be careful about mutating ScriptableObject values at runtime if you expect them to reset.

What is the CreateAssetMenu attribute?

It is an attribute that adds your ScriptableObject to the right-click Create menu in the Project window, making it easy to create instances without navigating through menus manually. Without it, creating asset instances is significantly more inconvenient.

Can I use ScriptableObjects as a save system?

They are not designed for this. ScriptableObjects are suitable for configuration data, but for persistent save data that should survive between game sessions use a proper serialization approach like JSON or a dedicated save system.

How many ScriptableObject instances can I create from one class?

As many as you need — each instance is an independent asset file with its own set of values. This is how item databases work: one WeaponData class definition, dozens of individual weapon asset instances.

Can ScriptableObjects communicate between scripts?

Yes, using the Game Events pattern described in this guide, where a ScriptableObject acts as a shared communication channel that scripts subscribe to or trigger without needing to reference each other directly.

Why are my enemies all dying at the same time?

Current health is almost certainly stored in a shared ScriptableObject rather than in each enemy's own MonoBehaviour. Move runtime state like currentHealth to the per-enemy script, initialized from the ScriptableObject's maxHealth at Start().

Can I use ScriptableObjects with the NavMesh system from earlier in this series?

Yes — storing NavMeshAgent speed, detection radius, and attack values in an EnemyData ScriptableObject, then applying them in the enemy's Start() method, is exactly the kind of use case ScriptableObjects are designed for.

Do ScriptableObjects work in Unity builds, not just the Editor?

Yes. ScriptableObject assets referenced by objects in your scenes are included in builds automatically, and read-only access to their values works identically in builds and in the Editor.

Key Takeaways

  • ScriptableObjects are data containers that live as project assets, shared between scripts without tight coupling.
  • They solve the hardcoded-values problem by moving shared configuration into one authoritative, Inspector-editable file.
  • Use them for configuration (stats, speeds, descriptions) not for per-instance runtime state (current health, current ammo).
  • The CreateAssetMenu attribute is essential for making ScriptableObjects easy to create and use in practice.
  • The Game Events pattern is an advanced but very practical use case for decoupled communication between scripts.
  • Changes to ScriptableObject assets during play mode persist after exiting play mode — always be aware of this.

Action Steps

  1. Create an EnemyData ScriptableObject class and one instance, then connect it to your enemy script from the NavMesh guide.
  2. Create a WeaponData ScriptableObject and build two or three weapon instances with different stats, then wire a simple inventory script to hold and equip them.
  3. Identify three hardcoded values in your current project that belong in a ScriptableObject and refactor them one at a time.
  4. Build the GameEvent system from this guide and replace one direct script-to-script function call in your project with a raised event.
  5. Save your ScriptableObject class definitions and asset instances to source control, and confirm a teammate (or a future you in another project) can create new instances from the Create menu.

Learning Roadmap

  1. Get comfortable creating and referencing basic ScriptableObject data containers using the EnemyData example.
  2. Build a small item database using WeaponData instances and a simple inventory that holds them.
  3. Refactor existing hardcoded values from your Character Controller and NavMesh scripts into ScriptableObject assets.
  4. Implement the GameEvent pattern for at least one inter-system communication point, like player death or scene load.
  5. Explore scene management and how ScriptableObjects provide a clean way to pass data between scenes without singletons.

Next Topics To Learn

No comments:

Post a Comment

How to Use ElevenLabs for Game Development: AI Voice Acting, NPC Dialogue, and Sound Design

Most indie games ship without voice acting. Not because developers do not want it, but because hiring voice actors is expensive, scheduling ...