이 페이지에서 얻을 수 있는 내용: 스크립터블 객체로 게임 코드를 설계하여 게임 코드를 쉽게 변경하고 디버깅할 수 있는 방법에 대한 팁입니다.
이 팁은 스크립터블 오브젝트를 통한 게임 설계 경험이 풍부한 Schell Games의 선임 엔지니어 라이언 히플(Ryan Hipple) 씨가 제공해 주셨습니다. 스크립터블 오브젝트에 대한 라이언의 유나이트 강연은 여기에서 시청할 수 있습니다. 또한 Unity 엔지니어인 리처드 파인(Richard Fine)의 스크립터블 오브젝트 입문 세션도 확인하시기 바랍니다.
스크립터블 오브젝트(ScriptableObject)는 스크립트 인스턴스와 별개로 대량의 공유 데이터를 저장할 수 있는 직렬화 가능한 Unity 클래스입니다. 스크립터블 오브젝트를 사용하면 변경 사항 및 디버깅을 더 쉽게 관리할 수 있습니다. 게임의 다양한 시스템 간에 유연한 커뮤니케이션 방식을 구축할 수 있으므로 제작 중에 언제든지 이러한 시스템을 간편하게 변경 및 조정할 수 있고 컴포넌트도 손쉽게 재사용할 수 있습니다.
모듈형 디자인 사용:
- 서로 직접적인 종속 관계가 있는 시스템을 만들지 마세요. 예를 들어 인벤토리 시스템은 게임 내 다른 시스템과 커뮤니케이션할 수 있어야 하지만, 시스템을 다른 설정 및 관계로 다시 구성하기가 어려워지기 때문에 시스템 간에 하드 레퍼런스를 생성하지 않는 것이 좋습니다.
- 씬을 클린 슬레이트로 생성: 씬 간에 임시 데이터가 존재하지 않도록 하세요. 씬을 히트할 때마다 씬이 완전히 브레이크된 후 새로 로드되어야 합니다. 그러면 해킹을 하지 않고도 다른 씬에 없는 고유한 동작이 포함된 씬을 생성할 수 있습니다.
- 프리팹이 자체적으로 작동하도록 설정하세요. 가능하면 씬으로 드래그하는 모든 프리팹이 각각의 기능을 포함하도록 해야 합니다. 그러면 팀의 규모가 상대적으로 커서 씬이 여러 프리팹으로 구성되며 각 프리팹이 개별적인 기능을 포함하는 경우에 소스를 컨트롤하기가 매우 수월해집니다. 이렇게 하면 대부분의 체크인이 프리팹 수준에서 이루어져 씬에서 충돌이 감소합니다.
- 각 컴포넌트를 한 가지 문제 해결에 집중적으로 사용하세요. 그러면 여러 컴포넌트를 조합하여 새로운 것을 더 쉽게 빌드할 수 있습니다.
변경과 수정이 쉽도록 제작하기:
- 게임을 최대한 데이터 중심적으로 제작하세요. 명령어를 통해 데이터를 처리하는 컴퓨터처럼 게임 시스템을 설계하면, 게임이 실행 중인 경우에도 변경 사항을 게임에 효율적으로 적용할 수 있습니다.
- 시스템을 최대한 모듈 및 컴포넌트 기반으로 설정하면 아티스트와 디자이너를 비롯한 사용자가 시스템을 더 쉽게 수정할 수 있습니다. 디자이너가 각각 한 가지 작업만 수행하는 작은 컴포넌트를 구현할 수 있으므로 별도의 기능을 요청할 필요 없이 게임 내 요소를 만들 수 있습니다. 이렇게 되면 디자이너가 구현한 컴포넌트를 다양한 방식으로 조합하여 새로운 게임플레이/메카닉스를 직접 개발할 수도 있습니다. 라이언은 팀의 게임에 포함된 많은 멋진 기능들이 이러한 프로세스를 거쳐 탄생했다고 말하며 이 프로세스를 "신생 디자인(emergent design)"이라고 부릅니다.
- 런타임 시 팀에서 변경 사항을 게임에 적용할 수 있는 것이 중요합니다. 런타임 시 게임을 변경할 수 있는 자유도가 높을수록 안정성과 가치도 높아지며, 스크립터블 오브젝트처럼런타임 상태를 외부에 다시 저장할 수 있으면 더 좋습니다.
디버깅이 쉽도록 제작하기:
이 요소는 앞 두 요소의 하위 요소입니다. 게임의 모듈화 수준이 높을수록 일부만 따로 테스트하기가 쉬워집니다. 게임을 수정할 수 있는 자유도가 높을수록, 즉 자체 인스펙터 뷰가 있는 기능이 많을수록 디버그하기가 더 쉽습니다. 인스펙터에서 디버그 상태를 볼 수 있도록 하고, 디버그 방식에 대한 계획을 수립하기 전까지는 기능이 완성됐다고 간주하지 마세요.
스크립터블 오브젝트로 빌드할 수 있는 가장 간단한 것 중 하나는 독립적인 에셋 기반 변수입니다. 아래의 예제는 FloatVariable의 예지만, 다른 직렬화 가능한 타입으로도 확장할 수 있습니다.
모든 팀원은 기술적 수준에 상관없이 새로운 FloatVariable 에셋을 생성하여 새로운 게임 변수를 정의할 수 있습니다. MonoBehaviour 또는 스크립터블 오브젝트는 이 새로운 공유 값을 참조하기 위해 public float 대신 public FloatVariable을 사용할 수 있습니다.
더 좋은 점은 MonoBehaviour 하나가 FloatVariable 값을 변경하면 다른 MonoBehaviour도 해당 변경 사항을 참조할 수 있다는 점입니다. 이 경우 시스템 간에 일종의 메시징 레이어가 생성되므로 서로 참조할 필요가 없습니다.
이에 대한 사용 사례로 플레이어의 체력 포인트(HP)를 들 수 있습니다. 로컬 싱글 플레이어 게임에서 플레이어의 HP는 이름이 PlayerHP인 FloatVariable일 수 있습니다. 플레이어가 데미지를 입으면 PlayerHP에서 차감되고 플레이어가 치유되면 PlayerHP에 가산됩니다.
이제 씬에 체력 바 프리팹이 있다고 가정해 봅시다. 체력 바는 PlayerHP 변수를 모니터링하여 디스플레이를 업데이트합니다. 코드를 변경하지 않아도 PlayerMP 변수와 같은 다른 변수를 쉽게 참조할 수 있습니다. 체력 바는 씬의 플레이어에 대한 아무런 정보도 갖고 있지 않으며, 플레이어가 기록하는 변수와 동일한 변수에서 값을 읽어올 뿐입니다.
이렇게 설정하고 나면 PlayerHP를 시청하기 위해 더 많은 항목을 추가하기가 쉽습니다. PlayerHP가 낮아짐에 따라 음악 시스템이 변경될 수 있고, 적들은 플레이어가 약하다는 것을 알 때 공격 패턴을 변경할 수 있으며, 화면 공간 효과는 다음 공격의 위험을 강조할 수 있습니다. 여기서 중요한 것은 플레이어 스크립트에서 이러한 시스템에 메시지를 보내지 않으며, 시스템이 플레이어 게임 오브젝트에 대해 알 필요도 없다는 점입니다. 또한 테스트를 위해 게임 실행 중에 인스펙터에 들어가서 PlayerHP의 값을 변경할 수도 있습니다.
FloatVariable의 값을 수정하는 경우 데이터를 런타임 값으로 복사하여 디스크에 스크립터블 오브젝트로 저장된 값을 변경하지 않는 것이 좋습니다. 이렇게 하면 MonoBehaviour가 디스크에 저장된 InitialValue를 수정하지 않도록 RuntimeValue에 액세스해야 합니다.
스크립터블 오브젝트에 빌드할 수 있는 기능 중에 라이언이 가장 즐겨 사용하는 것 중 하나는 이벤트 시스템입니다. 이벤트 아키텍처는 서로에 대한 직접적인 정보를 갖고 있지 않은 시스템 간에 메시지를 보내 코드를 모듈화하는 데 도움이 됩니다. 이를 통해 업데이트 루프에서 상태 변경을 계속 모니터링하지 않아도 상태 변경에 대응할 수 있습니다.
다음 코드 예제는GameEvent 스크립터블 오브젝트 및 GameEventListener MonoBehaviour의 두 부분으로 구성된 이벤트 시스템입니다. 디자이너는 중요 메시지를 나타내는 GameEvent를 프로젝트에 생성하여 보낼 수 있습니다. GameEventListener는 특정 GameEvent가 발생할 때까지 대기하고, 실제 이벤트가 아니라 직렬화된 함수 호출에 더 가까운 UnityEvent를 호출하여 응답합니다.
GameEvent ScriptableObject:
GameEventListener:
게임에서 플레이어의 죽음을 처리하는 것을 예로 들 수 있습니다. 아주 다양한 실행 관련 항목이 변경될 수 있는 지점이지만, 이 모든 로직을 어디에 코딩해야 하는지 결정하기 어려울 수 있습니다. 플레이어 스크립트가 게임 종료 UI나 음악 변경을 트리거해야 할까요? 적이 프레임마다 플레이어가 아직 살아있는지 확인해야 할까요? 이벤트 시스템을 활용하면 이와 같은 종속성 문제를 해결할 수 있습니다.
플레이어가 죽으면 플레이어 스크립트가 OnPlayerDied 이벤트에 대해 Raise를 호출합니다. 플레이어 스크립트는 단순히 브로드캐스트일 뿐이기 때문에 어떤 시스템이 이벤트와 연관되어 있는지 알 필요가 없습니다. 게임 오버(Game Over) UI는 OnPlayerDied 이벤트에 반응하여 애니메이션화를 시작하며, 카메라 스크립트는 화면을 검은색으로 채우기 시작할 수 있으며, 음악 시스템은 음악을 변경할 수 있습니다. 적 캐릭터도 각각 OnPlayerDied 이벤트에 반응하도록 하여 도발 애니메이션이나 대기 동작으로 돌아가는 상태 변경을 트리거할 수 있습니다.
이 패턴에 따라 플레이어의 죽음에 대한 새로운 리스폰스를 매우 쉽게 추가할 수 있습니다. 또한, 특정 테스트 코드에서 이벤트에 대해 Raise를 호출하거나 인스펙터의 버튼을 사용하여 플레이어의 죽음에 대한 리스폰스를 쉽게 테스트할 수도 있습니다.
Schell Games에서 빌드한 이벤트 시스템은 훨씬 더 복잡하면서 데이터를 전달하고 타입을 자동으로 생성할 수 있는 기능이 있는 시스템으로 발전했습니다. 이 예제는 현재 Schell Games에서 사용하고 있는 시스템의 시초입니다.
스크립터블 오브젝트는 단순 데이터가 아니어도 됩니다. MonoBehaviour를 통해 구현한 시스템을 스크립터블 오브젝트로도 구현할 수 있는지 확인해 보세요. InventoryManager를 DontDestroyOnLoad MonoBehaviour에 두는 대신 스크립터블 오브젝트에 둘 수도 있습니다.
씬에 연결되지 않았기 때문에 Transform이 없고 Update 함수를 가져오지 않지만, 특별히 초기화하지 않아도 씬이 로드되는 동안 상태를 유지합니다. 인벤토리에 액세스하기 위한 스크립트가 필요한 경우 싱글톤 대신 인벤토리 시스템 오브젝트에 대한 공용 레퍼런스를 사용하세요. 그러면 싱글톤을 사용하는 경우보다 테스트 인벤토리나 튜토리얼 인벤토리를 바꿔 넣기가 쉬워집니다.
이제 플레이어 스크립트가 인벤토리 시스템을 참조하는 것을 가정해 볼 수 있습니다. 플레이어가 생성될 때 시스템이 플레이어가 소유하고 있던 모든 오브젝트를 인벤토리에 요청하고 장비를 생성할 수 있습니다. 착용 UI도 인벤토리를 레퍼런스하고 여러 아이템을 루프한 다음 무엇을 드로우할지 결정할 수 있습니다.