Scirptable Object
ScriptableObject(SO)는 Unity에서 제공하는 데이터 컨테이너 클래스이다.
MonoBehaviour와 달리 GameObject에 부착되지 않는 독립적인 데이터 에셋이다.
즉, Prefab과 같이 에디터상에 유일하게 존재하는 것이다.
또한, Scene과 독립적으로 존재하며 런타임에 변경된 내용을 정적으로 저장하여 가져갈 수 있다.
Scene 전환 시에도 데이터가 유지되며 한 번 로드된 SO는 메모리에 캐싱되어 재사용할 수 있다.
빌드 시 에셋으로 포함되어 배포되게 된다.
Unity 에디터에서 직접 데이터를 수정하고 저장할 수 있기 때문에 밸런싱같은 작업에서 사용하기 매우 좋다.
Data Container SO
SO는 기본적으로 데이터 컨테이너이다.
즉, 순수하게 데이터만 저장할 목적이라면 순수 C#클래스와 동일하다는 뜻이다.
단, 에디터에서 값을 변경할 수 있기 때문에 굳이 C#클래스로 만들 이유가 없다.
예를 들어, 캐릭터 스탯을 저장하는 데이터 클래스가 필요하다고 가정해 보자.
[CreateAssetMenu(fileName = "CharacterStats", menuName = "RPG/CharacterStats")]
public class CharacterStatsSO : ScriptableObject
{
public float baseHealth;
public float baseMana;
public float moveSpeed;
public float attackPower;
[System.Serializable]
public class LevelUpBonus
{
public float healthBonus;
public float manaBonus;
public float attackBonus;
}
public LevelUpBonus levelUpStats;
}
// 실제 사용
public class PlayerCharacter : MonoBehaviour
{
[SerializeField] private CharacterStatsSO baseStats;
private int currentLevel = 1;
private float currentHealth;
private void Start()
{
// 기본 스탯으로 초기화
currentHealth = baseStats.baseHealth + (baseStats.levelUpStats.healthBonus * (currentLevel - 1));
}
}
CharacterStatsSo를 통해 캐릭터의 스탯을 에디터상에서 미리 설정해 놓고 사용할 수 있다.
만약, 캐릭터 스탯이 변경되지 않는 값이라면 같은 값을 갖는 수많은 인스턴스를 만들어도 해당 SO를 공유해서 사용하게 하여 메모리 사용량을 줄일 수 있다.
Event SO
SO를 통해 이벤트를 핸들링하는 것도 가능하다.
만약, 특정 이벤트가 다른 클래스와 많은 연관이 있는 경우 참조가 꼬이거나 의존성 문제가 발생할 수 있다.
예를 들어, 전투 상황에서 플레이어가 받은 데미지를 UI에 표시해야 하는 상황을 생각해 보자.
SO를 사용하지 않는다면 플레이어의 Stat의 변화를 감지하거나 혹은 명시적으로 UI의 함수를 호출해야 하기 때문에 플레이어와 UI의 의존성은 필수이다.
SO를 사용하면 이러한 의존성을 풀어낼 수 있다.
[CreateAssetMenu(fileName = "CombatEvent", menuName = "RPG/Events/CombatEvent")]
public class CombatEventSO : ScriptableObject
{
private List<Action<CombatData>> listeners = new List<Action<CombatData>>();
public void Raise(CombatData data)
{
for (int i = listeners.Count - 1; i >= 0; i--)
listeners[i]?.Invoke(data);
}
public void AddListener(Action<CombatData> listener) => listeners.Add(listener);
public void RemoveListener(Action<CombatData> listener) => listeners.Remove(listener);
}
// 전투 시스템에서 사용
public class CombatSystem : MonoBehaviour
{
[SerializeField] private CombatEventSO onDamageDealt;
public void DealDamage(Character attacker, Character target, float damage)
{
var combatData = new CombatData(attacker, target, damage);
onDamageDealt.Raise(combatData); // 데미지 이벤트 발생
}
}
// UI에서 구독
public class CombatUI : MonoBehaviour
{
[SerializeField] private CombatEventSO onDamageDealt;
private void OnEnable()
{
onDamageDealt.AddListener(ShowDamagePopup);
}
private void ShowDamagePopup(CombatData data)
{
// 데미지 팝업 표시
}
}
SO를 정의하고 SO의 Action에 구독하게 되면 해당 SO가 Invoke 될 때 구독한 함수가 실행되기 때문에 UI는 플레이어를 직접 참조할 필요가 없다.
서로 공통된 SO를 설정하면 된다.
MonoBehaviour와 비교
SO는 MonoBehaviour와 달리 GameObject에 부착되지 않는 독립적인 데이터 에셋이다.
따라서, MonoBehaviour와 같은 라이프 사이클을 거치지 않는다.
SO는 독립적인 라이프 사이클이 존재한다.
// MonoBehaviour - GameObject에 부착
public class PlayerController : MonoBehaviour
{
void Update() { /* 매 프레임 실행되는 게임 로직 */ }
}
// ScriptableObject - 데이터 저장
[CreateAssetMenu(fileName = "PlayerData", menuName = "RPG/PlayerData")]
public class PlayerDataSO : ScriptableObject
{
public float maxHealth; // 에디터에서 설정하는 데이터
public float moveSpeed;
}
- 초기화: Unity 에디터에서 생성 시 또는 런타임에 로드 시
- 유지: Scene 전환과 관계없이 메모리에 유지
- 해제: 명시적으로 Destroy 호출하거나 앱 종료 시