Có nên dùng ScriptableObject làm Runtime Data Container?
Minh Khoa
Tác giả
Câu trả lời ngắn: Dùng ScriptableObject cho dữ liệu dùng chung thì rất tốt. Dùng nó để giữ HP hiện tại của từng Enemy thì sớm muộn cũng gặp bug.
> ## Vấn đề không nằm ở ScriptableObject
Giả sử game có ba con Goblin cùng sử dụng EnemyData.asset:
public class Enemy : MonoBehaviour
{
[SerializeField] private EnemyData data;
public void TakeDamage(int damage)
{
data.currentHealth -= damage;
}
}
Nhìn qua thì khá gọn. Nhưng cả ba Enemy không giữ ba bản EnemyData khác nhau. Chúng đang cùng trỏ tới một asset duy nhất.
Kết quả:
Goblin A bị đánh
↓
EnemyData.currentHealth: 100 → 70
↓
Goblin B và Goblin C cũng còn 70 HP
Đây không phải lỗi reference của Unity. ScriptableObject được thiết kế để nhiều object cùng sử dụng một bản dữ liệu, giúp tránh tạo các bản copy không cần thiết.
Lỗi nằm ở việc chúng ta đã biến Config dùng chung thành State của từng instance.
Config và Runtime State khác nhau thế nào?
Cách phân biệt đơn giản nhất:
- Config trả lời: “Đối tượng này vốn là gì?”
- Runtime State trả lời: “Hiện tại đối tượng này đang thế nào?”
Ví dụ với một Enemy:
| Config dùng chung | Runtime State riêng |
|---|---|
| Max HP: 100 | Current HP: 37 |
| Damage: 20 | Đang bị choáng |
| Move Speed: 4 | Target hiện tại |
| Drop Table | Thời gian hồi chiêu |
Ba Goblin có thể dùng chung Max HP, Damage và Move Speed. Nhưng mỗi con phải có Current HP riêng.
Cấu trúc an toàn hơn
Giữ ScriptableObject ở trạng thái chỉ đọc:
[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;
}
Sau đó tạo một class C# riêng cho 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);
}
}