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
Tạo class kế thừa ScriptableObject
Thêm [CreateAssetMenu(menuName="Game/ItemData")] để menu xuất hiện trong Assets → Create.
Tạo Asset trong Project
Chuột phải → Assets → Create → Game → ItemData. Đặt tên theo dữ liệu (vd: Sword_Data.asset).
Điền dữ liệu trong Inspector
Click vào file .asset → Inspector hiện fields → điền stats, sprites, sounds...
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.
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.
Ví dụ Code
Cơ bảnTạ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 caoSO 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>()