Unity Term Book
Scripting & Lifecycle

ScriptableObject

ScriptableObject là container dữ liệu độc lập — không gắn vào GameObject, lưu như Asset, và được nhiều đối tượng chia sẻ mà không cần sao chép dữ liệu mỗi lần.

Hãy tưởng tượng...

ScriptableObject giống như menu thực đơn nhà hàng. Thực đơn (SO) chứa thông tin món ăn — tên, giá, mô tả. Mọi bàn (GameObject) đều tham chiếu cùng 1 thực đơn, không ai cầm bản copy riêng. Khi cần update giá, chỉ cần sửa thực đơn gốc → tất cả bàn thấy ngay. Thực đơn không biết ai đang đọc nó — nó chỉ lưu dữ liệu.

Khái niệm chi tiết

ScriptableObject kế thừa UnityEngine.Object (không phải MonoBehaviour). Tạo bằng [CreateAssetMenu] attribute → xuất hiện trong menu Assets → Create. File được lưu dạng .asset trong thư mục Assets.

Tại sao không dùng static hoặc [SerializeField]? Static không serialize, không Inspector-friendly. [SerializeField] trong MonoBehaviour tạo bản copy riêng cho mỗi instance → tốn bộ nhớ, khó sync. SO: một nguồn duy nhất, nhiều tham chiếu.

Use cases: Item/Enemy stats (WeaponData, EnemyStats), Game configuration (AudioSettings, InputConfig), Channel Events (tránh coupling), State containers (GameStateSO). SO không chạy Update/Awake nhưng có OnEnable/OnDisable.

Lưu ý Editor vs Runtime: Thay đổi SO trong Editor Mode được lưu vĩnh viễn. Thay đổi trong Play Mode cũng được lưu (không reset như MonoBehaviour). Trong Build, SO reset theo asset gốc khi khởi động lại game.

SO vs MonoBehaviour data

❌ [SerializeField] on MonoBehaviour

Enemy_01 (MonoBehaviour)

hp: 100, speed: 3

← bản copy riêng

Enemy_02 (MonoBehaviour)

hp: 100, speed: 3

← bản copy riêng

Enemy_03 (MonoBehaviour)

hp: 100, speed: 3

← bản copy riêng

3 copies × N bytes = lãng phí bộ nhớ

✅ ScriptableObject

GoblinData.asset (SO)

hp: 100, speed: 3

↙ ↓ ↘

Enemy_01

→ ref SO

Enemy_02

→ ref SO

Enemy_03

→ ref SO

1 copy duy nhất — sửa SO gốc = cập nhật tất cả

Hướng dẫn thực hành

1

Tạo class kế thừa ScriptableObject

Thêm [CreateAssetMenu(menuName="Game/ItemData")] để menu xuất hiện trong Assets → Create.

2

Tạo Asset trong Project

Chuột phải → Assets → Create → Game → ItemData. Đặt tên theo dữ liệu (vd: Sword_Data.asset).

3

Điền dữ liệu trong Inspector

Click vào file .asset → Inspector hiện fields → điền stats, sprites, sounds...

4

Tham chiếu từ MonoBehaviour

[SerializeField] private ItemData _data; → kéo asset vào Inspector.

Trình mô phỏng tương tác

Chọn một ScriptableObject Asset, chỉnh sửa các field và xem tất cả đối tượng sử dụng nó cập nhật ngay lập tức.

SO Assets:

Inspector (ScriptableObject)

Đối tượng dùng SO này

📌 Shared Reference

Tất cả đối tượng trên đều tham chiếu đến cùng 1 asset. Chỉnh sửa 1 lần → tất cả cập nhật.

Selected: GoblinData.asset|Thay đổi field để xem cập nhật realtime

Ví dụ Code

Cơ bản

Tạo WeaponData ScriptableObject và dùng trong MonoBehaviour.

using UnityEngine;

// Tạo 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
{
  // Kéo .asset file từ Project vào đây trong Inspector
  [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;  // swap tức thì — chỉ đổi reference
  }
}

Ví dụ Code

Nâng cao

SO làm Event Channel — giải coupling giữa Broadcaster và Listener.

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

// Event Channel — Asset trong Project, không phụ thuộc 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: không biết ai đang lắng nghe
public class PlayerDeath : MonoBehaviour
{
  [SerializeField] private VoidEventSO onPlayerDied;
  public void Die() => onPlayerDied.Raise();
}

// Listener: không biết ai raise event
public class GameOverUI : MonoBehaviour
{
  [SerializeField] private VoidEventSO onPlayerDied;
  void OnEnable()  => onPlayerDied.Subscribe(ShowGameOver);
  void OnDisable() => onPlayerDied.Unsubscribe(ShowGameOver);
  void ShowGameOver() => gameObject.SetActive(true);
}

📌 Ghi nhớ nhanh

  • SO = data container không gắn vào scene
  • Nhiều object tham chiếu 1 SO — không copy dữ liệu
  • SO thay đổi trong Play Mode → lưu vĩnh viễn (Editor)
  • SO Event Channel = decoupled pub/sub không cần scene ref

⚠️ Lỗi thường gặp

  • ❌ Sửa SO runtime trong Production Build

    SO shared — thay đổi ảnh hưởng TẤT CẢ object đang dùng nó

    ✅ Clone SO khi cần instance-specific data: Instantiate(soAsset)

  • ❌ Quên [CreateAssetMenu]

    Không tạo được .asset file từ Editor menu

    ✅ Thêm attribute, hoặc dùng ScriptableObject.CreateInstance<T>()