Even Idiots Can Make Game

먼치킨

Date/Lastmod
Section
PORTFOLIO

#1 프로젝트 개괄

#1 주요 기여 사항

#2 전체 기획서 작성

게임 Munchkin은 하찮고 멍청해 보이는 “닭"이 주인공인 고난이도 2.5D 플랫포머 게임입니다.

플레이어블 캐릭터인 “닭"은 극도로 나약한 생물로, 작은 자극에도 기절하거나 죽어 버립니다. 게다가 수명조차 짧아, 아무런 실수를 하지 않았더라도 시간이 지나면 늙어 죽고 맙니다.

하지만 이 미물에게도 한 가지 특별한 능력이 있습니다. 닭은 죽으면 영혼이 되어 가장 가까운 알로 날아갑니다. 게임 월드에는 반드시 하나 이상의 알이 존재하며, 닭의 영혼이 알에 닿으면 닭은 다시 병아리로 부화해 새로운 삶을 시작합니다. 시간이 흐르면 병아리는 닭으로 성장하고, 닭 상태에서는 새로운 알을 월드에 하나 배치할 수 있습니다.

알은 기본적으로 일회용 부활 지점이면서, 닭이 살아서 도달할 수 없는 곳에 던져서 플랫포밍의 돌파구로 활용할 수도 있습니다. 하지만 알을 배치할 때에는 신중해야 합니다. 알은 쉽게 굴러가고 충격에 약해 금방 깨져 버립니다.

만약 플레이어가 월드에 배치한 알이 깨져 버리면, 월드의 시작 지점인 “둥지"에 있는 알이 활성화됩니다. 즉, 처음부터 다시 도전해야 한다는 뜻이죠.

world-map

Munchkin은 기본적으로 어려운 게임입니다. 플레이어가 조작하는 닭은 온갖 이유로 허무하게 실패하고 죽으며, 이러한 극적인 실패의 순간이 게임이 내세우는 주요 코미디 요소 중 하나입니다.

하지만 이 게임은 실패하는 것에 관한 게임은 아닙니다. 반복되는 실패 속에서 플레이어가 “닭"과 “알"의 생애주기를 이해하고, 그것이 환경과 상호작용 하는 법칙들을 학습하면서 포기하지 않고 나아가, 결국에는 승리를 거머쥐는 쾌감을 느끼게 하는 게임입니다.

character-concept

전체 기획 문서들은 아래와 같습니다

#2 동적 타일맵 로딩 시스템 구현

tilemap_dynamic_loading

명시적인 로딩 스크린이 존재하지 않는 거대한 월드를 구축하는 것이 목표였습니다.

또한, 플랫포밍이 가능한 지형은 Unity TileMap 컴포넌트의 GameObject 브러시를 활용해 테스트와 수정이 용이하도록 했습니다.

다음과 같이 구현하였습니다.

[참고] Unity에서 존재하는 SceneAssetUnityEditor에 존재하고, 빌드 게임에서는 사용할 수 없으므로 “씬"의 레퍼런스 직렬화를 관리하기 위해 외부 애셋(SceneReference.csSceneReferencePropertyDrawer.cs)을 사용하였습니다. (출처)

SceneDependencyData

위처럼 에디터에서 미리 씬간의 인접 관계를 정의합니다.

씬의 로드/언로드는 비동기적(AsyncOperation)으로 이루어지는 데에 반해, 로드/언로드 요청은 OnTriggerEnter라는 물리 엔진의 콜백으로 발생하기 때문에, 이미 비동기 작업이 진행 중인 씬에 중복된 요청을 처리하는 것이 관건이었습니다.

다음과 같은 해결책으로 문제를 해결하였습니다.

  1. isLoadingisUnloading 플래그로 AsyncOperation이 작업 중인 씬에 중복 로드/언로드 연산이 발생하지 않도록 방지
  2. 다양한 에디터 스크립트를 활용하여 현재 로드/언로드 중인 씬과, 로드된 씬, 그리고 로드된 씬 별로 해당 씬을 로드한 게임오브젝트를 한 눈에 확인할 수 있게 하였습니다.

SceneLoadManager

또한 궁극적으로 모든 씬은 Tilemap 컴포넌트의 GameObject 브러시를 활용한 3D 타일로 구축될 것입니다. 때문에 많은 작은 게임오브젝트에 대해서 로드와 언로드가 매끄럽게 진행되길 원했고, 스트레스 테스트를 진행하였습니다. 에디터에서는 씬 로드/언로드가 일어날 때마다 약간의 프레임 드랍이 있었지만, 빌드 버전에서는 매끄럽게 진행되었습니다.

#2 고유의 생애 주기를 갖는 알 오브젝트 구현

캐릭터는 생애주기 내에 월드에 알이라는 특수한 오브젝트를 한 번 배치할 수 있습니다. 알은 Rigidbody를 사용하여 물리 효과를 받으며 굴러다니고, 일정량 이상의 충격을 받으면 깨져서 사라집니다.

알은 다양한 이유로 깨질 수 있고, 체력도 존재해야 하며, 고유의 생애주기를 관리해야 했습니다.

PlantUML Diagram

위와 같은 형태로, EggLifecycleHandler부터 시작하여 관찰자 패턴을 적극 활용하여 컴포넌트끼리 느슨한 연결을 갖도록 하되, 필요한 정보를 전달받을 수 있게 구성했습니다.

EggLifecycleHandler는 알의 운명의 결정권한을 가지는 가장 중요한 컴포넌트입니다. 알 인스턴스의 상태를 정리해서 Object Pool에 반납하거나 Object Pool에서 꺼내진 알 인스턴스를 초기화합니다.1

EggHealthManager는 알의 체력을 관리합니다. 체력은 알이 생애주기를 종료하게 되는 하나의 요인으로서 설계되었으며, 그 자신은 체력의 실제 수치 변화를 다른 컴포넌트에 알리기도 합니다. 예를 들어, 알은 보유한 체력에 따라 외형이 변화하는데, 해당 외형 변화에 관여하는 컴포넌트는 EggHealthManager를 구독하면 됩니다.

체력의 변경을 데미지라고 정의하고, EggDamageSourceBase는 알의 체력에 영향을 줄 수 있는 요인들의 기초 클래스로 설계했습니다. 예를 들어, 알에 가해진 물리적인 충격 중 임계값을 넘는 것을 데미지로 변환하는 EggPhysicalDamageSource가 있습니다.

또한 알의 생애주기를 에디터에서 편리하게 관리하기 위해 다음처럼 에디터 스크립트를 작성하기도 하였습니다.

추가적으로, 알과 깨진 알을 모델링하고, 알이 체력에 따라 깨진 알 노멀 맵의 알파값을 증가시켜, 알이 점점 깨져가는 것 처럼 보이게 하는 셰이더 그래프를 작성하였습니다.

#2 2.5D 플랫포밍 캐릭터 컨트롤러 구현

게임 기획의 핵심 컨셉이 들어있는 캐릭터를 구현하였습니다. 크게 캐릭터가 입력을 받아 플랫포밍 행동으로 전환하는 스크립트와, 캐릭터의 생애주기에 따른 상태를 관리하는 스크립트를 작성하였습니다.

캐릭터 게임 오브젝트는 다음과 같은 구조로 구성됩니다.

캐릭터의 로직과 로직에 따른 비주얼(데이터)를 명확히 구분하였습니다. ScriptableObject를 이용해 캐릭터의 비주얼 세트를 정의하고 이를 런타임에 인스턴싱 하도록 구현하여, 아티스트의 작업이 진행됨에 따라서 캐릭터의 비주얼을 쉽게 교체할 수 있도록 만들었습니다.

character visual data

이 게임에서 캐릭터는 함정에 데미지를 입는 것 외에도, 게임 플레이 시간에 따라서 수명을 먹고 노화하며 외형과 상태를 변경합니다. 이를 관장하는 스크립트는 LifespanHandler이며, 알 오브젝트를 구현한 것과 마찬가지로 관찰자 패턴을 이용하여 캐릭터 생애주기의 시작과 변화, 끝이 발생하였음을 그 구독자 스크립트들에게 알립니다.

캐릭터 수명의 각 시점에서 발생하는 사건들을 에디터에서 통제해야 할 일이 많았으므로, 다른 캐릭터 액션과 함께 전용 에디터 스크립트를 작성하여 쉽게 수명을 조절할 수 있도록 만들었습니다.

character lifespan editor

character action editor


  1. 오브젝트 풀링은 UnityEngine.Pool을 활용했습니다. 하지만 오브젝트 풀링이 알 오브젝트 구현에 반드시 필요한 요소는 아니었습니다. 특히, 객체를 풀에서 가져오거나 반납하는 주체를 결정하고, 그 과정에서 반납 전 객체의 상태 정리와 같은 추가적인 엔지니어링 비용을 부담해야 했습니다. 디자인 패턴의 성급한 적용은 비효율을 초래할 수 있다는 것을 배울 수 있었던 좋은 기회였습니다. ↩︎

comments powered by Disqus