무엇을 찾고 계신가요?
Hero background image
Last updated January 2020, 10 min. read

스크립터블 오브젝트로 게임을 설계하는 세 가지 방법

이 웹페이지는 이해를 돕기 위해 기계 번역으로 제공됩니다. 기계 번역으로 제공되는 콘텐츠에 대한 정확도나 신뢰도는 보장되지 않습니다. 번역된 콘텐츠의 정확도에 관해 의문이 있는 경우 웹페이지의 공식 영어 원문을 참고해 주시기 바랍니다.

이 페이지에서 얻을 수 있는 것: 게임 코드를 쉽게 변경하고 디버그할 수 있도록 Scriptable Objects로 아키텍처를 설계하는 방법에 대한 팁입니다.

이 팁은 Ryan Hipple, Schell Games의 수석 엔지니어로, 게임 아키텍처에 Scriptable Objects를 사용하는 고급 경험을 가진 사람에게서 나왔습니다. Ryan의 Scriptable Objects에 대한 Unite 강연을 여기<4>에서 시청할 수 있습니다; Unity 엔지니어 Richard Fine의 Scriptable Objects에 대한 훌륭한 소개<5> 세션도 추천합니다.

스크립터블 오브젝트란?

ScriptableObject는 스크립트 인스턴스와 독립적으로 대량의 공유 데이터를 저장할 수 있게 해주는 직렬화 가능한 Unity 클래스입니다. 스크립터블 오브젝트를 사용하면 변경 사항 및 디버깅을 더 쉽게 관리할 수 있습니다. 게임의 다양한 시스템<1> 간에 유연한 통신 수준을 구축할 수 있어, 제작 과정에서 이를 더 쉽게 변경하고 조정할 수 있으며, 구성 요소를 재사용할 수 있습니다.

게임 엔지니어링의 3대 요소

모듈형 디자인 사용:

  • 서로 직접적으로 의존하는 시스템을 만들지 마십시오. 예를 들어, 인벤토리 시스템은 게임의 다른 시스템과 통신할 수 있어야 하지만, 이들 간에 하드 참조를 만들고 싶지 않습니다. 이는 시스템을 서로 다른 구성과 관계로 재조립하기 어렵게 만듭니다.
  • 장면을 깨끗한 슬레이트로 만드십시오: 장면 간에 일시적인 데이터가 존재하지 않도록 하십시오. 씬을 히트할 때마다 씬이 완전히 브레이크된 후 새로 로드되어야 합니다. 이렇게 하면 다른 장면에 존재하지 않았던 고유한 동작을 가진 장면을 가질 수 있으며, 해킹을 할 필요가 없습니다.
  • 프리팹을 설정하여 독립적으로 작동하도록 하십시오. 가능하면 씬으로 드래그하는 모든 프리팹이 각각의 기능을 포함하도록 해야 합니다. 그러면 팀의 규모가 상대적으로 커서 씬이 여러 프리팹으로 구성되며 각 프리팹이 개별적인 기능을 포함하는 경우에 소스를 컨트롤하기가 매우 수월해집니다. 그렇게 하면 대부분의 체크인은 프리팹 수준에서 이루어져 장면에서의 충돌이 줄어듭니다.
  • 각 구성 요소가 단일 문제를 해결하는 데 집중하도록 하십시오. 이렇게 하면 여러 구성 요소를 조합하여 새로운 것을 만드는 것이 더 쉬워집니다.

부품을 쉽게 변경하고 편집할 수 있도록 하십시오:

  • 게임의 가능한 한 많은 부분을 데이터 기반으로 만드십시오. 게임 시스템을 데이터 처리 기계처럼 설계하면, 게임이 실행 중일 때도 효율적으로 변경할 수 있습니다.
  • 시스템이 가능한 한 모듈화되고 구성 요소 기반으로 설정되어 있다면, 아티스트와 디자이너를 포함하여 이를 편집하기가 더 쉬워집니다. 디자이너가 명시적인 기능을 요청하지 않고 게임에서 사물을 조합할 수 있다면 – 각기 하나의 작업만 수행하는 작은 구성 요소를 구현한 덕분에 – 그들은 새로운 게임 플레이/메커니즘을 찾기 위해 이러한 구성 요소를 다양한 방식으로 결합할 수 있습니다. Ryan은 그의 팀이 게임에서 작업한 가장 멋진 기능 중 일부가 이 과정에서 나왔다고 말하며, 이를 "출현 디자인"이라고 부릅니다.
  • 팀이 런타임에 게임을 변경할 수 있는 것이 중요합니다. 런타임에 게임을 더 많이 변경할 수록, 균형과 값을 찾을 수 있으며, Scriptable Objects처럼 런타임 상태를 저장할 수 있다면, 좋은 위치에 있는 것입니다.

디버그를 쉽게 하세요:

이것은 실제로 첫 두 가지의 하위 기둥입니다. 게임의 모듈화 수준이 높을수록 일부만 따로 테스트하기가 쉬워집니다. 게임을 수정할 수 있는 자유도가 높을수록, 즉 자체 인스펙터 뷰가 있는 기능이 많을수록 디버그하기가 더 쉽습니다. 인스펙터에서 디버그 상태를 볼 수 있도록 하고, 디버그할 계획이 없을 때는 기능이 완성되었다고 생각하지 마세요.

변수 설계

ScriptableObjects로 만들 수 있는 가장 간단한 것 중 하나는 자가 포함된 자산 기반 변수입니다. 아래는 FloatVariable의 예이지만, 이는 다른 직렬화 가능한 유형으로도 확장됩니다.

팀의 모든 사람은 기술적이지 않더라도 새로운 FloatVariable 자산을 생성하여 새로운 게임 변수를 정의할 수 있습니다. 모든 MonoBehaviour 또는 ScriptableObject는 이 새로운 공유 값을 참조하기 위해 공용 float 대신 공용 FloatVariable을 사용할 수 있습니다.

더 나아가, 하나의 MonoBehaviour가 FloatVariable의 값을 변경하면, 다른 MonoBehaviour도 그 변경을 볼 수 있습니다. 이것은 서로 참조할 필요가 없는 시스템 간의 메시징 레이어를 생성합니다.

예: 플레이어의 건강 포인트

예: 플레이어의 건강 포인트

이것의 예시 사용 사례는 플레이어의 체력 포인트(HP)입니다. 로컬 싱글 플레이어 게임에서 플레이어의 HP는 이름이 PlayerHP인 FloatVariable일 수 있습니다. 플레이어가 피해를 입으면 PlayerHP에서 차감되고, 플레이어가 치유되면 PlayerHP에 추가됩니다.

이제 장면에 체력 바 프리팹을 상상해 보세요. 체력 바는 PlayerHP 변수를 모니터링하여 디스플레이를 업데이트합니다. 코드를 변경하지 않아도 PlayerMP 변수와 같은 다른 변수를 쉽게 참조할 수 있습니다. 체력 바는 장면의 플레이어에 대해 아무것도 알지 못하며, 플레이어가 쓰는 동일한 변수에서 읽기만 합니다.

이렇게 설정되면 PlayerHP를 감시할 더 많은 것을 추가하기가 쉽습니다. 플레이어 HP가 낮아지면 음악 시스템이 변경될 수 있으며, 적들은 플레이어가 약하다는 것을 알면 공격 패턴을 변경할 수 있고, 화면 공간 효과는 다음 공격의 위험성을 강조할 수 있습니다. 여기서 중요한 것은 플레이어 스크립트에서 이러한 시스템에 메시지를 보내지 않으며, 시스템이 플레이어 게임 오브젝트에 대해 알 필요도 없다는 점입니다. 게임이 실행 중일 때 인스펙터에서 PlayerHP의 값을 변경하여 테스트할 수도 있습니다.

FloatVariable의 값을 편집할 때, ScriptableObject에 저장된 값을 변경하지 않기 위해 데이터를 런타임 값으로 복사하는 것이 좋습니다. 이렇게 하면 MonoBehaviour는 디스크에 저장된 InitialValue를 편집하지 않도록 RuntimeValue에 접근해야 합니다.

이벤트 설계

Ryan이 ScriptableObjects 위에 구축하는 것을 좋아하는 기능 중 하나는 이벤트 시스템입니다. 이벤트 아키텍처는 서로에 대한 직접적인 정보를 갖고 있지 않은 시스템 간에 메시지를 보내 코드를 모듈화하는 데 도움이 됩니다. 이들은 업데이트 루프에서 지속적으로 모니터링하지 않고도 상태 변화에 반응할 수 있게 해줍니다.

다음 코드 예제는 GameEvent ScriptableObject와 GameEventListener MonoBehaviour의 두 부분으로 구성된 이벤트 시스템에서 가져온 것입니다. 디자이너는 중요 메시지를 나타내는 GameEvent를 프로젝트에 생성하여 보낼 수 있습니다. GameEventListener는 특정 GameEvent가 발생하기를 기다리고 UnityEvent를 호출하여 응답합니다(이는 진정한 이벤트가 아니라 직렬화된 함수 호출에 가깝습니다).

코드 예제: GameEvent ScriptableObject

GameEvent ScriptableObject:

코드 예제: GameEventListener

GameEventListener:

플레이어의 죽음을 처리하는 이벤트 시스템

플레이어의 죽음을 처리하는 이벤트 시스템

이것의 예는 게임에서 플레이어의 죽음을 처리하는 것입니다. 아주 다양한 실행 관련 항목이 변경될 수 있는 지점이지만, 이 모든 로직을 어디에 코딩해야 하는지 결정하기 어려울 수 있습니다. 플레이어 스크립트가 게임 종료 UI나 음악 변경을 트리거해야 할까요? 적이 프레임마다 플레이어가 아직 살아있는지 확인해야 할까요? 이벤트 시스템은 이러한 문제의존성을 피할 수 있게 해줍니다.

플레이어가 죽으면 Player 스크립트가 OnPlayerDied 이벤트에서 Raise를 호출합니다. 플레이어 스크립트는 단순히 브로드캐스트일 뿐이기 때문에 어떤 시스템이 이벤트와 연관되어 있는지 알 필요가 없습니다. 게임 오버(Game Over) UI는 OnPlayerDied 이벤트에 반응하여 애니메이션화를 시작하며, 카메라 스크립트는 화면을 검은색으로 채우기 시작할 수 있으며, 음악 시스템은 음악을 변경할 수 있습니다. 각 적이 OnPlayerDied를 듣고, 조롱 애니메이션을 트리거하거나 대기 행동으로 돌아가는 상태 변화를 일으킬 수 있습니다.

이 패턴은 플레이어의 죽음에 대한 새로운 반응을 추가하는 것을 매우 쉽게 만듭니다. 또한, 테스트 코드나 인스펙터의 버튼에서 이벤트에서 Raise를 호출하여 플레이어의 죽음에 대한 반응을 쉽게 테스트할 수 있습니다.

Schell Games에서 구축한 이벤트 시스템은 훨씬 더 복잡한 것으로 발전하였으며, 데이터 전달 및 자동 생성 유형을 허용하는 기능이 있습니다. 이 예제는 그들이 오늘 사용하는 것의 본질적인 출발점이었습니다.

기타 시스템 설계

Scriptable Objects는 단순한 데이터일 필요는 없습니다. MonoBehaviour를 통해 구현한 시스템을 스크립터블 오브젝트로도 구현할 수 있는지 확인해 보세요. DontDestroyOnLoad MonoBehaviour에 InventoryManager를 두는 대신, ScriptableObject에 넣어보세요.

장면에 묶여 있지 않기 때문에 Transform이 없고 Update 함수도 받지 않지만, 특별한 초기화 없이 장면 로드 간 상태를 유지합니다. 인벤토리에 액세스하기 위한 스크립트가 필요한 경우 싱글톤 대신 인벤토리 시스템 오브젝트에 대한 공용 레퍼런스를 사용하세요. 테스트 인벤토리 또는 튜토리얼 인벤토리를 교체하는 것이 싱글톤을 사용하는 것보다 쉽습니다.

여기에서 플레이어 스크립트가 인벤토리 시스템에 대한 참조를 가져오는 것을 상상할 수 있습니다. 플레이어가 생성될 때 시스템이 플레이어가 소유하고 있던 모든 오브젝트를 인벤토리에 요청하고 장비를 생성할 수 있습니다. 장비 UI는 인벤토리를 참조하고 아이템을 반복하여 무엇을 그릴지 결정할 수 있습니다.

스크립터블 오브젝트로 코드를 효율적으로 변경하고 디버깅할 수 있도록 설계 | Unity