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
Create a ScriptableObject class
Add [CreateAssetMenu(menuName="Game/ItemData")] so it appears under Assets → Create.
Create an Asset in the Project
Right-click → Assets → Create → Game → ItemData. Name it after the data (e.g. Sword_Data.asset).
Fill fields in the Inspector
Select the .asset → Inspector shows fields → fill stats, sprites, sounds…
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.
Inspector (ScriptableObject)
Objects using this SO
📌 Shared Reference
Every object above references the same asset. Edit once → all update.
Code example
BasicCreate 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
AdvancedSO 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>()