Unity Term Book
Scripting & Lifecycle

ScriptableObject

ScriptableObject is a standalone data container — not attached to a GameObject, saved as an Asset, and shared by many objects without copying the data.

Imagine...

A ScriptableObject is like a restaurant menu. The menu (SO) holds dish info — name, price, description. Every table (GameObject) references the same menu; nobody keeps a private photocopy. Update a price on the master menu → every table sees it instantly. The menu does not know who is reading — it only stores data.

The concept in detail

A ScriptableObject inherits UnityEngine.Object (not MonoBehaviour). Create it with [CreateAssetMenu] → it appears under Assets → Create. Files are stored as .asset under Assets.

Why not static or [SerializeField]? Static does not serialize and is Inspector-unfriendly. [SerializeField] on a MonoBehaviour copies data per instance → more memory, hard to sync. SO: one source, many references.

Use cases: Item/Enemy stats (WeaponData, EnemyStats), game config (AudioSettings, InputConfig), event channels (loose coupling), state containers (GameStateSO). SOs do not run Update/Awake but do get OnEnable/OnDisable.

Editor vs Runtime: Edits in Edit Mode persist. Edits in Play Mode also persist in the Editor (unlike MonoBehaviour fields). In a player build, SO values reset from the asset when the game restarts.

SO vs MonoBehaviour data

❌ [SerializeField] on MonoBehaviour

Enemy_01 (MonoBehaviour)

hp: 100, speed: 3

← private copy

Enemy_02 (MonoBehaviour)

hp: 100, speed: 3

← private copy

Enemy_03 (MonoBehaviour)

hp: 100, speed: 3

← private copy

3 copies × N bytes = wasted memory

✅ ScriptableObject

GoblinData.asset (SO)

hp: 100, speed: 3

↙ ↓ ↘

Enemy_01

→ ref SO

Enemy_02

→ ref SO

Enemy_03

→ ref SO

One shared copy — edit the SO → everyone updates

Hands-on steps

1

Create a ScriptableObject class

Add [CreateAssetMenu(menuName="Game/ItemData")] so it appears under Assets → Create.

2

Create an Asset in the Project

Right-click → Assets → Create → Game → ItemData. Name it after the data (e.g. Sword_Data.asset).

3

Fill fields in the Inspector

Select the .asset → Inspector shows fields → fill stats, sprites, sounds…

4

Reference from a MonoBehaviour

[SerializeField] private ItemData _data; → drag the asset into the Inspector.

Interactive simulator

Pick a ScriptableObject asset, edit its fields, and watch every consumer update instantly.

SO Assets:

Inspector (ScriptableObject)

Objects using this SO

📌 Shared Reference

Every object above references the same asset. Edit once → all update.

Selected: GoblinData.asset|Change a field to see realtime updates

Code example

Basic

Create a WeaponData ScriptableObject and use it from a MonoBehaviour.

using UnityEngine;

// Menu: Assets → Create → Game/Data → Weapon Data
[CreateAssetMenu(menuName = "Game/Data/Weapon Data", fileName = "New Weapon")]
public class WeaponData : ScriptableObject
{
  public string     weaponName;
  public int        damage;
  public float      attackSpeed;
  public Sprite     icon;
  public AudioClip  swingSound;
}

public class PlayerWeapon : MonoBehaviour
{
  // Drag the .asset from Project onto this field
  [SerializeField] private WeaponData currentWeapon;

  public void Attack()
  {
      Debug.Log($"Attacking with {currentWeapon.weaponName}: {currentWeapon.damage} dmg");
      AudioSource.PlayClipAtPoint(currentWeapon.swingSound, transform.position);
  }

  public void EquipWeapon(WeaponData newWeapon)
  {
      currentWeapon = newWeapon;  // instant swap — only the reference changes
  }
}

Code example

Advanced

SO as an Event Channel — decouple broadcaster and listener.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

// Event Channel — Project asset, independent of scene hierarchy
[CreateAssetMenu(menuName = "Events/Void Event")]
public class VoidEventSO : ScriptableObject
{
  private List<UnityAction> _listeners = new();

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

  public void Subscribe(UnityAction listener)   => _listeners.Add(listener);
  public void Unsubscribe(UnityAction listener) => _listeners.Remove(listener);
}

// Broadcaster: does not know who is listening
public class PlayerDeath : MonoBehaviour
{
  [SerializeField] private VoidEventSO onPlayerDied;
  public void Die() => onPlayerDied.Raise();
}

// Listener: does not know who raised the event
public class GameOverUI : MonoBehaviour
{
  [SerializeField] private VoidEventSO onPlayerDied;
  void OnEnable()  => onPlayerDied.Subscribe(ShowGameOver);
  void OnDisable() => onPlayerDied.Unsubscribe(ShowGameOver);
  void ShowGameOver() => gameObject.SetActive(true);
}

📌 Quick recap

  • SO = data container not tied to a scene
  • Many objects reference one SO — no data copies
  • SO edits in Play Mode persist in the Editor
  • SO Event Channel = decoupled pub/sub without scene refs

⚠️ Common mistakes

  • ❌ Mutating an SO at runtime in a production build

    Shared SO — the change hits EVERY object using it

    ✅ Clone when you need per-instance data: Instantiate(soAsset)

  • ❌ Forgetting [CreateAssetMenu]

    Cannot create a .asset from the Editor menu

    ✅ Add the attribute, or use ScriptableObject.CreateInstance<T>()