Under Construction
Unity8 min98 views

Should you use ScriptableObject it as a Runtime Data Container?

Minh Khoa

Minh Khoa

Author

Short answer: It ScriptableObject is excellent for shared data. Use it to keep each Enemy's current HP, and you'll eventually run into a bug.

image.png> ## The problem is not with ScriptableObject

Suppose the game has three Goblins all using EnemyData.asset:

public class Enemy : MonoBehaviour
{
    [SerializeField] private EnemyData data;

    public void TakeDamage(int damage)
    {
        data.currentHealth -= damage;
    }
}

At first glance, it looks fairly neat. But the three Enemies do not hold three EnemyData different copies. They are all pointing to one single asset.

Result:

Goblin A bị đánh
        ↓
EnemyData.currentHealth: 100 → 70
        ↓
Goblin B và Goblin C cũng còn 70 HP

This is not a Unity reference bug. ScriptableObject is designed for multiple objects to share one data copy, helping avoid unnecessary duplication.

The mistake is that we turned shared config into per-instance state.

How are Config and Runtime State different?

The simplest way to distinguish them:

  • Config answers: “What is this object inherently?”
  • Runtime State answers: “What is this object like right now?”

For example, with an Enemy:

Shared configSeparate runtime state
Max HP: 100Current HP: 37
Damage: 20Is stunned
Move Speed: 4Current target
Drop TableCooldown time

Three Goblins can share Max HP, Damage and Move Speed. But each one must have Current HP its own.

A safer structure

Keep ScriptableObject in read-only mode:

[CreateAssetMenu(menuName = "Game/Enemy Config")]
public class EnemyConfig : ScriptableObject
{
    [SerializeField] private int maxHealth;
    [SerializeField] private int damage;

    public int MaxHealth => maxHealth;
    public int Damage => damage;
}

Then create a separate class C# for the state:

public sealed class EnemyState
{
    public int CurrentHealth { get; private set; }

    public EnemyState(EnemyConfig config)
    {
        CurrentHealth = config.MaxHealth;
    }

    public void TakeDamage(int damage)
    {
        CurrentHealth = Mathf.Max(0, CurrentHealth - damage);
    }
}

Each Enemy creates its own state:

public class Enemy : MonoBehaviour
{
    [SerializeField] private EnemyConfig config;

    private EnemyState state;

    private void Awake()
    {
        state = new EnemyState(config);
    }
}

Now the three Goblins still share one EnemyConfig, but they have three EnemyState independent

Traps between the Editor and the Build

When you Play in the Editor, you are referencing the real asset in the Project. Editing data through the Inspector or Editor tooling can change the asset that still exists after you exit Play Mode.

In a Build, changes still take effect in the session's memory, but it is not a data-saving system. Turn the game off and reopen it, and that state will be lost.

So:

If the data needs to persist across multiple play sessions, put it into Save/Load with JSON, a database, or a suitable storage format. Do not treat file .asset as the player's save file.

When is mutable SO still reasonable?

ScriptableObject holding runtime state is not always wrong. It can be appropriate if that state really must be shared, such as the state of a game session or a runtime blackboard.

But then you need:

  • A single place responsible for writing.
  • A clear reset rule when a session starts.
  • Do not write directly to the original asset.
  • You can clone it with Instantiate(originalSO) during bootstrap if you still want to work with SO.

If multiple systems such as Combat, UI and Quest are all allowed to modify the same SO without an owner, you have created global mutable state — convenient at first, but very hard to debug later.

How to remember

ScriptableObject = Bản thiết kế dùng chung
Runtime State    = Tình trạng của từng instance
Save Data        = Dữ liệu tồn tại qua nhiều phiên chơi

ScriptableObject is not dangerous. On the contrary, using it in the right role helps keep the game architecture clean, saves memory, and data-driven more.

Bugs usually start when we no longer distinguish which one is the design copy and which one is the object that is alive in the game.