Unity 검색

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

최근 업데이트: 2018년 12월

이 페이지의 내용: 스크립터블 오브젝트를 통한 설계로 게임 코드를 간편하게 관리, 변경, 디버그할 수 있는 방법에 대한 유용한 강의입니다.

이들 팁은 Schell Games의 선임 엔지니어인 라이언 히플(Ryan Hipple)이 제공합니다. 라이언은 스크립터블 오브젝트를 사용하여 다양한 게임을 설계했습니다. 스크립터블 오브젝트에 관한 라이언의 유나이트 토크는 여기에서 시청할 수 있습니다. 또한 Unity 엔지니어인 리처드 파인(Richard Fine)의 스크립터블 오브젝트 소개 세션도 놓치지 마세요. 라이언, 정말 수고했습니다!

스크립터블 오브젝트를 간단히 설명한다면?

스크립터블 오브젝트는 스크립트 인스턴스에 관계 없이 대량의 공유 데이터를 저장하는 데 사용할 수 있는 직렬화 가능한 Unity 클래스입니다. 스크립터블 오브젝트를 사용하면 변경 사항과 디버깅을 관리하기가 더 쉬워집니다. 진행하면서 필요한 것을 더 쉽게 변경하고 적절히 수정할 수 있도록 게임 내 여러 시스템 사이에 유연한 커뮤니케이션 레벨을 빌드하고 컴포넌트를 재사용할 수도 있습니다.

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

모듈 구조 유지

  • 서로 직접적인 종속 관계가 있는 시스템을 만들지 않도록 하세요. 예를 들어 인벤토리 시스템은 게임 내 다른 시스템과 통신할 수 있어야 하지만, 시스템 간에 하드 레퍼런스를 생성하지 않는 것이 좋습니다. 시스템을 다른 설정 및 관계로 다시 구성하기가 어려워지기 때문입니다.
  • 씬을 클린 슬레이트로 생성: 씬 간에 임시 데이터가 존재하지 않도록 하세요. 씬을 히트할 때마다 씬이 완전히 중단된 후 새롭게 로드되어야 합니다. 그러면 해킹을 하지 않고도 다른 씬에 없는 고유한 동작이 포함된 씬을 생성할 수 있습니다.
  • 각 프리팹에 기능을 포함하세요. 가능하면 씬으로 드래그하는 모든 프리팹이 각각의 기능을 포함하도록 해야 합니다. 그러면 팀의 규모가 상대적으로 커서 씬이 여러 프리팹으로 구성되며 각 프리팹이 개별적인 기능을 포함하는 경우에 소스를 컨트롤하기가 매우 수월해집니다. 이렇게 하면 대부분의 체크인이 프리팹 수준에서 이루어져 씬에서 충돌이 감소합니다.
  • 각 컴포넌트를 한 가지 문제 해결에 집중적으로 사용하세요. 그러면 여러 컴포넌트를 조합하여 새롭게 빌드하기가 더 쉬워집니다.

편집 가능하도록 제작

  • 게임을 최대한 데이터 기반 구조로 제작하세요. 명령어를 통해 데이터를 처리하는 컴퓨터처럼 게임 시스템을 설계하면, 게임이 실행 중인 경우에도 변경 사항을 게임에 더 효율적으로 적용할 수 있습니다.
  • 시스템을 최대한 모듈 및 컴포넌트 기반으로 설정하면 아티스트와 디자이너 등이 더 쉽게 편집할 수 있습니다. 디자이너가 각각 한 가지 작업만 수행하는 작은 컴포넌트를 구현할 수 있다면 별도의 기능을 요청하지 않아도 되며, 이로 인해 스스로 편집을 수행할 수 있습니다. 이렇게 되면 디자이너가 구현한 컴포넌트를 다양한 방식으로 조합하여 새로운 게임플레이/메카닉스를 직접 개발할 수도 있습니다. 라이언은 팀의 게임들에 포함된 많은 멋진 기능들이 이러한 프로세스를 거쳐 탄생했다고 합니다. 라이언은 이 프로세스를 "신생 디자인(emergent design)"이라고 부릅니다.
  • 런타임에 팀이 변경 사항을 게임에 적용할 수 있는 것이 중요합니다. 런타임에 게임을 수정할 수 있는 여지가 커지는 만큼 안정성과 가치도 커집니다. 스크립터블 오브젝트처럼 런타임 상태를 외부에 다시 저장할 수 있으면 더욱 좋을 것입니다.

디버그 가능하도록 제작

처음 두 가지의 하위 요소라고 볼 수 있습니다. 게임의 모듈성이 높을수록 일부만 따로 테스트하기가 쉬워집니다. 게임을 수정하기가 더 쉬울수록, 즉 자체 인스펙터 뷰가 있는 기능이 많을수록 디버그하기가 더 쉽습니다. 인스펙터에서 디버그 상태를 볼 수 있도록 하고, 디버그 방식에 대한 계획을 수립하기 전까지는 절대로 할 일이 끝났다고 간주하지 마세요.

라이언이 소개하는 '스크립터블 오브젝트로 빌드할 수 있는 가장 멋진 것 세 가지'

변수

스크립터블 오브젝트(Scriptable Object)로 빌드할 수 있는 가장 간단한 것 중 하나는 독립적인 에셋 기반 변수입니다. 다음은 FloatVariable의 예지만, 다른 직렬화 가능한 타입으로도 확장할 수 있습니다.

Unity 스크립터블 오브젝트 플로트 변수

이를 통해 모든 팀원은 기술적 수준에 상관 없이 새로운 FloatVariable 에셋을 생성하여 새로운 게임 변수를 정의할 수 있습니다. MonoBehaviour 또는 스크립터블 오브젝트는 이 공유 값을 참조하기 위해 공용 플로트 대신 공용 FloatVariable을 사용할 수 있습니다.

더 좋은 점은 MonoBehaviour 하나가 FloatVariable 값을 변경하면 다른 MonoBehaviour도 해당 변경 사항을 확인할 수 있다는 점입니다. 이 경우 시스템 간에 일종의 메시징 레이어가 생성되므로 서로에 대한 참조가 필요하지 않습니다.

이에 대한 예로 플레이어의 HP를 들 수 있습니다. 로컬 플레이어 1인용 게임에서 플레이어의 HP는 이름이 PlayerHP인 FloatVariable일 수 있습니다. 플레이어가 데미지를 입으면 PlayerHP에서 차감되고 플레이어가 치유되면 PlayerHP에 가산됩니다.

이제 씬에 상태 표시줄 프리팹이 있다고 상상해 보세요. 상태 표시줄은 PlayerHP 변수를 모니터링하여 디스플레이를 업데이트합니다. 코드를 변경하지 않으면 다른 것(예: PlayerMP 변수)을 포인트하기 쉽습니다. 상태 표시줄은 씬의 플레이어에 대한 아무런 정보도 갖고 있지 않으며, 플레이어가 기록하는 변수와 동일한 변수에서 값을 읽어올 뿐입니다.

Unity 스크립터블 오브젝트로 플레이어의 죽음 처리

이렇게 설정해 두면 PlayerHP와 연동할 요소를 추가하기가 쉽습니다. PlayerHP가 낮아지면 음악이 바뀔 수 있고, 플레이어가 약해진 것을 알면 적의 공격 패턴이 변할 수 있으며, 다음 공격을 받으면 위험하다는 상태를 화면 효과로 알릴 수 있습니다. 여기서 중요한 것은 플레이어(Player) 스크립트에서 이러한 시스템에 메시지를 보내지 않으며, 시스템이 플레이어 게임 오브젝트에 대해 알 필요도 없다는 점입니다. 게임 실행 중에 인스펙터에 들어가서 PlayerHP의 값을 변경하여 테스트할 수도 있습니다.

FloatVariable의 값을 편집하는 경우 데이터를 런타임 값으로 복사하고 디스크에 ScriptableObject로 저장된 값을 변경하지 않는 것이 좋습니다. 이렇게 하면 MonoBehaviours가 RuntimeValue에 액세스하여 디스크에 저장된 InitialValue를 편집하는 것을 방지하게 됩니다.

Unity 스크립터블 오브젝트 플로트 변수

이벤트

스크립터블 오브젝트 위에 빌드할 수 있는 기능 중에 필자가 가장 즐겨 사용하는 것 중 하나는 이벤트 시스템입니다. 이벤트 아키텍처는 서로에 대한 직접적인 정보를 갖고 있지 않은 시스템 간에 메시지를 보내 코드를 모듈화하는 데 도움이 됩니다. 이를 통해 업데이트 루프에서 상태 변경을 계속 모니터링하지 않아도 상태 변경에 대응할 수 있습니다.

이 이벤트 시스템은 GameEvent ScriptableObject와 GameEventListener MonoBehaviour로 구성됩니다. 디자이너는 중요 메시지를 나타내는 GameEvent를 프로젝트에 생성하여 보낼 수 있습니다. GameEventListener는 특정 GameEvent가 발생할 때까지 대기하고 UnityEvent(실제 이벤트가 아니고 직렬화된 함수 호출에 더 가까움)를 호출하여 대응합니다.

Unity 스크립터블 오브젝트 게임 이벤트 리스너

Unity 스크립터블 오브젝트 게임 이벤트 리스너

Unity 스크립터블 오브젝트 게임 이벤트 리스너

예로는 게임에서 플레이어의 죽음을 처리하는 것을 들 수 있습니다. 이 시점에 아주 다양한 실행 관련 항목이 바뀔 수 있지만, 각 로직을 코딩할 시점을 결정하기가 어려울 수 있습니다. Player 스크립트가 게임 종료 UI나 음악 변경을 트리거해야 할까요? 적이 매 프레임마다 플레이어가 아직 살아있는지 확인해야 할까요? 이벤트 시스템을 활용하면 이와 같은 종속성 문제를 해결할 수 있습니다.

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

이 패턴에 따라 플레이어의 죽음에 대한 새로운 리스폰스를 매우 쉽게 추가할 수 있습니다. 또한, 특정 테스트 코드에서 이벤트에 대해 Raise를 호출하거나 인스펙터의 버튼을 사용하여 플레이어의 죽음에 대한 리스폰스를 쉽게 테스트할 수도 있습니다.

Unity 스크립터블 오브젝트로 플레이어의 죽음 처리

Schell Games에서 필자가 빌드한 이벤트 시스템은 훨씬 더 복잡하면서 데이터를 전달하고 타입을 자동으로 생성할 수 있는 기능이 있는 시스템으로 발전했습니다. 세부적인 내용을 모두 공개할 수는 없지만, 현재 저희가 사용하고 있는 시스템의 시초라고 말씀드릴 수 있습니다.

시스템

Scriptable Object는 단순 데이터가 아니어도 됩니다. MonoBehaviour에서 아무 시스템이나 선택하여 구현을 ScriptableObject로 이동할 수 있는지 확인하세요. InventoryManager를 DontDestroyOnLoad MonoBehaviour에 포함시키지 않고 ScriptableObject에 대신 놓으려고 시도해 보세요.

씬에 연결되지 않았기 때문에 Transform이 없고 Update 함수를 가져오지 않지만, 특별히 초기화하지 않아도 서로 다른 씬이 로드되는 동안 상태를 유지합니다. 인벤토리에 액세스하기 위한 스크립트가 필요한 경우 싱글톤 대신 인벤토리 시스템 오브젝트에 대한 공용 레퍼런스를 사용하세요. 그러면 싱글톤을 사용하는 경우보다 테스트 인벤토리나 튜토리얼 인벤토리를 바꿔 넣기가 쉬워집니다.

이제 Player 스크립트가 인벤토리 시스템을 참조하는 것을 가정해 볼 수 있습니다. 플레이어가 스폰될 때 시스템이 플레이어가 소유한 모든 오브젝트의 인벤토리를 요청하고 장비를 스폰할 수 있습니다. 착용 UI도 인벤토리를 참조하고 여러 아이템을 루프한 다음 무엇을 드로우할지 결정할 수 있습니다.

리소스 더 보기

Unity 버프보트로 편하게 작업 진행

신청을 통해 Unity 전문가로부터 매주 기술 및 크리에이티브 노하우를 받으세요.

구독
확인

당사에서는 웹 사이트의 모든 기능을 최대로 이용할 수 있도록 쿠키를 사용합니다. 자세한 정보는 여기를 클릭하세요.