다음은 'Three ways to architect your game with ScriptableObjects' 포스팅을 해석하고 풀이한 글입니다.
ScriptableObjects란 무엇인가?ScriptableObject는 serialize 가능한 Unity class로, script 인스턴스로부터 독립적인 공유 데이터를 많이 저장할 수 있다. ScriptableObjects를 이용하여 변화와 디버깅을 쉽게 할 수 있다.
게임 엔지니어링의 세 가지 축
1. 모듈러 설계를 사용하라
- 서로 직접 의존하는 시스템을 지양하라. (디커플링)
- Scene을 백지 상태로 생성하라. (Scene 간 데이터 공유 지양)
- Prefab을 독립적으로 만들어라. (Prefab은 그 자체로 기능할 수 있어야한다. Scene은 단지 여러 prefab의 모음으로 보이게끔.)
- 각 컴포넌트를 하나의 문제를 해결하는데 집중하라. (이건 무슨 말인지 모르겠다..)
2. 부품(parts)을 바꾸고, 수정하기 쉽게 하라
- 가능한 게임을 데이터 기반으로 만들어라.
- 시스템을 가능한 모듈화하고, 컴포넌트 기반으로 만들면 수정하기 쉽다. 아티스트와 디자이너도.
- 게임을 런타임에 수정할 수 있다는 것은 매우 중요하다.
3. 디버그하기 쉽게 하라
게임을 모듈화할수록, 어떤 단일 영역도 테스트하기 쉬워진다. 게임을 수정하기 쉬워질수록, 디버그하기도 쉬워진다.
변수 설계
ScriptableObject를 가장 간단히 사용할 수 있는 방법 중 하나는 자급자족적인, 에셋 기반 변수이다. 아래 예제에서는 하나의 새로운 float 변수로 참조를 위해 public float을 사용하는 대신, 아래의 public FloatVariable 타입의 변수를 생성/사용한다. 어느 MonoBehaviour에서 FloatVariable의 값을 바꾸게 되면, 참조하는 다른 MonoBehaviour들은 그 변화를 알 수 있다. 이는 시스템 간 서로 직접적인 참조가 필요없는 메시징 레이어를 만들어 준다.
# FloatVariable.cs
[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
public float Value;
}
Example: Player's Health Points
플레이어는 FloatVariable 타입의 PlayerHP 변수를 갖고 있고, 이 변수는 플레이어가 데미지를 입었을 때 감소하고, 힐을 할 때 증가한다.
Scene에 Health Bar 프리팹이 있다고 상상해보자. 이 Health Bar는 PlayerHP의 변화를 표시하기 위해 이 변수를 모니터링 하고 있다. Health Bar는 Scene에 존재하는 플레이어 자체에 대해 아무것도 알고 있는 것이 없다. 단지 플레이어가 쓰는 변수만 읽고 있다.
시스템을 이런 식으로 구성하면 위 사진처럼 PlayerHP를 관찰하는 다른 것들을 쉽게 추가할 수 있다. PlayerHP가 낮아졌을 때 달라지는 뮤직 시스템, 플레이어가 약해졌을 때 변하는 Enemy 공격 패턴(Enemy AI) 등.
중요한 것은 Player 스크립트는 이러한 시스템들에 아무런 메시지도 전달하지 않고, 반대로 이런 시스템들은 Player 오브젝트에 대해 알 필요가 없다는 것이다.
FloatVariable의 값을 수정할 때, ScriptableObject에 저장된 오리지널 값을 보존하기 위해 runtime value로 복사해 사용하는 것이 좋다.
# RuntimeValue.cs
[CreateAssetMenu]
public class FloatVariable : ScriptableObject, ISerializationCallbackReceiver
{
public float InitialValue;
[NonSerialized]
public float RuntimeValue;
public void OnAfterDeserialize()
{
RuntimeValue = InitialValue;
}
public void OnBeforeSerialize() { }
}
이벤트 설계
이벤트 아키텍처는 서로를 모르는 시스템 간 메시지를 주고 받음으로써 코드를 모듈화한다. 그것은 업데이트 루프에서 끊임없이 관찰하지 않고도 상황 변화에 반응할 수 있도록 해준다.
아래 예시 코드는 GameEvent와 GameEventListener로 구성된 이벤트 시스템을 보여준다. GameEventListener는 특정 GameEvent가 Raise되는 것을 기다리고, UnityEvent를 Invoke하는 것으로 반응한다.
# GameEvent ScriptableObject.cs
[CreateAssetMenu]
public class GameEvent : ScriptableObject
{
private List<GameEventLister> listeners = new List<GameEventListener>();
public void Raise()
{
for(int i = listeners.Count = 1; i >= 0; i--)
{
listeners[i].OnEventRaised();
}
}
public void RegisterListener(GameEventListener listener)
{
listeners.Add(listener);
}
public void UnregisterListener(GameEventListener listener)
{
listeners.Remove(listener);
}
}
# GameEventListener.cs
public class GameEventListener : MonoBehaviour
{
public GameEvent Event;
public UnityEvent Response;
private void OnEnable()
{
Event.RegisterListener(this);
}
private void OnDisable()
{
Event.UnregisterListener(this);
}
public void OnEventRaised()
{
Response.Invoke();
}
}
플레이어 Death를 컨트롤하는 이벤트 시스템
이벤트 시스템의 한 예로 플레이어 Death를 살펴보자. 플레이어가 Death 상태가 될 때 플레이어 script는 Game Over UI나 음악 변경 시스템을 트리거 해야하는가? 적들은 매 프레임마다 플레이어가 살았는지 체크해야 하는가? 이벤트 시스템은 이러한 문제가 있는 의존성을 방지하게 한다.
플레이어가 죽을 때, 플레이어 script는 OnPlayerDied 이벤트의 Raise 메서드를 호출한다. 플레이어 script는 단순히 플레이어의 죽음을 알리기만 할 뿐 어떤 시스템들이 관여하는지 알 필요가 없다. Game Over UI와 music 시스템은 OnPlayerDied 이벤트를 listening하다가 각자 반응한다.
다른 시스템을 위한 설계
ScriptableObject는 단순히 데이터용일 필요는 없다. MonoBehaviour에서 상속받아 사용하는 어느 시스템이든 ScriptableObject로 가져올 수 있는지 판단해 보라. 가령 MonoBehaviour DontDestroyOnLoad를 사용하는 InventoryManager를 ScriptableObject로 사용해보라.
ScriptableObject는 Scene에 종속적이지 않기 때문에, Transform을 가질 필요가 없고 Update 메서드도 필요없지만, 어떤 별도의 초기화도 없이 scene load간 상태를 유지한다. 싱글턴 대신에, inventory 시스템에서 inventory에 접근할 script가 필요할 때 public reference를 사용하라. 이는 싱글턴보다 테스트 inventory나 튜토리얼 inventory에서 쉽게 바꿀 수 있다.
Inventory 시스템을 참조하고 있는 플레이어 script가 있다고 해보자. 플레이어가 생성될 때, 그것은 Inventory에 소유한 모든 오브젝트와 모든 장비에 대한 생성을 요구할 수 있다. 장비 UI는 또한 Inventory를 참조하고 어떤 아이템을 그려야할 지 결정하기 위해 루프를 돌 수 있다.
참고
Unite 2016 - Overthrowing the MonoBehaviour Tyranny in a Glorious Scriptable Object Revolution - www.youtube.com/watch?v=6vmRwLYWNRo&ab_channel=Unity
'Unity & C# > Scripting' 카테고리의 다른 글
[Effective C#] 1장. C# 언어 요소(아이템 1~5) (0) | 2021.01.29 |
---|---|
Data based structure - ScriptableObject (0) | 2020.11.06 |
C# Extension Method(확장 메서드) (0) | 2020.08.14 |