프로젝트 깃허브 주소
https://github.com/woorija/3D_RPG
프로젝트 소개
플레이 영상
프로젝트 개발에 사용된 기능
- Addressable
- 그동안 진행했던 프로젝트는 런타임중 변동이 필요한 리소스를 사용할 때 Resource 폴더를 사용해서 런타임중에 리소스를 로드하는 방식을 주로 사용했다. 이 방법은 사용하기는 되게 간단하나, 빌드할 때 Resource내에 있는 모든 에셋이 포함되어 빌드용량이 커진다는 단점이 있다.
- 또한, 요즘 상용게임들은 설치 할 때 모든 데이터를 다운로드 받는게 아니라, 앱을 설치하고, 인앱에서 추가 패치를 받는 방식을 사용하기 때문에 어드레서블을 사용하게 되었다.
- 어드레서블은
- UniTask
- 기존 프로젝트들은 코루틴을 사용하여 비동기 작업을 처리했고, FlatTouch에 블록체인을 접목시킨 사이드 프로젝트에서 써드파티 라이브러리를 사용하기 위해 Task로 비동기 작업을 처리했다.
- 최적화에 신경쓰게 되면서 자료들을 찾아보니 코루틴은 StartCoroutine함수를 사용할 때, yield return new wait… 함수를 사용할 때 GC가 발생한다는 것을 알았고, yieldcache클래스를 만들어 new wait… 호출을 줄인다 해도 StartCoroutine으로 인한 GC는 해결할 수 없다는 것을 확인했다. 또한 코루틴은 예외처리와 복잡한 비동기 작업이 어렵다는 단점이 존재한다.
- 그러다 찾은게 UniTask다. UniTask는 Task를 구조체로 구현하여 GC를 최소한으로 줄이고, 유니티에서 자주 쓰이는 에셋인 Addressable, DoTween도 사용 가능하고 예외처리도 사용가능하여 코루틴과 Task의 장점을 흡수한 라이브러리라고 할 수 있다.
- Input System
- 그동안 진행했던 프로젝트는 Input.GetKey() 또는 Input.GetMouseButton()을 사용해왔다. 그동안 개발해온 게임들은 비교적 조작키가 적은 게임이라 위 함수를 사용하는게 편했으나, 플레이어 조작 방식을 FSM으로 선택하고 개발을 하다 보니 각 플레이어 상태의 update구문에 위 함수들을 사용하는게 많이 복잡해져 Input System을 사용하게 되었다.
- 또한 Input System은 별도의 함수를 추가하지 않고 InputAction의 Actions에 다양한 입력장치를 바인딩하여 간단하게 여러 플랫폼을 지원할 수 있다. 이 프로젝트는 여러 플랫폼에서 구동 되게 만들 계획은 없으나, Input System을 경험해봄으로 Input을 사용할 때 보다 멀티 플랫폼 지원을 어렵지 않게 적용할 수 있을거란 생각이 들었다.
- FSM
- RPG 장르의 플레이어블 캐릭터는 수많은 종류의 애니메이션을 사용하며, 모션에 따른 별도의 추가적인 액션도 필요하다. 그래서 디펜스 게임의 유닛 클래스처럼 애니메이션과 이동, 공격 등의 행동을 한 클래스에 몰아서 작성하면 구조가 너무 복잡해져서 유지보수가 매우 어렵게 될 것이라고 생각했다.
- 그래서 플레이어블 캐릭터의 구조를 FSM으로 작성했고, 각 상태를 별도의 클래스로 작성하고, 상태마다 추가적인 변수를 사용하여 플레이어의 애니메이션과 물리효과를 제어하거나 다른 상태로 변환하게 설계하였다. 또한 각 상태에 우선순위를 두어 동작 전환이 이상해지지 않게 제어하였다.
- Behavior Tree
- 몬스터의 구조도 FSM으로 구현하려고 생각했으나 몬스터는 플레이어와는 다르게 직접 조작하는 것이 아닌, 알고리즘을 이용해 조작해야 하며, 몬스터의 종류도 많고, 몬스터마다 공격 종류, 범위가 다 제각각이기 때문에 조건에 맞게 각각의 State를 연결하는 것이 복잡해지기 때문에 다른 방법으로 구현하는 것이 유지보수를 위해 더 좋겠다는 생각이 들었다.
- 그렇게 구조를 찾아보면서 공부하게 된 것이 Behavior Tree이다. Behavior Tree는 언리얼 엔진에선 기본적으로 내장되어 있으며, 유니티엔진은 기본적으로 내장되어 있지는 않지만 에셋스토어에서 좋은 에셋을 여럿 찾을 수 있었다. 다만, 이 프로젝트에서는 에셋을 사용하지 않고 Behavior Tree를 직접 구현했다. 직접 구현한 이유는 아래와 같다.
- 몬스터 AI를 돌리는데 자주 사용하게 될 것 같은 구조이기 때문에 직접 구현 해보면서 원리를 이해하기 위함이다.
- 직접 구현을 하면 원하는 기능을 생각하는 대로 구현할 수 있다.
- Behavior Tree의 노드는 Succese, Failure, Running 상태를 반환한다는 특성이 존재하고 Behavior Tree에서 루트노드를 실행하여 조건에 따라 하위 노드를 실행한다. 이 경우 Running 상태의 노드에 진입했을 경우 다른 상태를 반환할 때 까지 루트노드에서 해당 노드까지 진입하는 과정을 반복하게 된다.
이 과정에서 낭비되는 연산을 줄이기 위해 직접 구현한 Behavior Tree에는 루트노드 이외에 러닝노드 변수를 추가하여 Running 상태에 진입한 노드를 러닝노드 변수에 할당하여 Behavior Tree에서 러닝노드가 null이면 루트노드를 실행하고 러닝노드가 할당되어있으면 바로 러닝노드를 실행하게 하였다. 다만 이 경우엔 러닝 상태의 노드가 실행 중이면 블랙보드에 변화가 생겨도 다른 노드를 실행하지 못하기 때문에 다른 행동으로 진입하지 못한다는 문제가 생긴다.
이 문제를 해결하기 위해 이벤트를 이용하여 러닝노드를 캔슬하는 기능을 구현하여 다른 행동으로 바로 진입이 가능하게 하였다.
- Composite Node는 등록 순서대로 실행된다. 하지만 게임에서는 항상 같은 순서대로 패턴을 실행하는 것이 아니라 확률에 따라 다른 패턴을 실행하는 경우도 있다. RandomSelectorNode
- Custom Editor
- 위의 여러 기능을 구현하면서 생각한 것이 있다. 어드레서블의 경우 라벨을 추가할 때 마다 해당 스크립트에 라벨명과 같은 string변수를 배열에 추가해야하며, Behavior Tree의 경우 몬스터 종류마다 필요한 노드도 다르고, 적용해야할 BT와 애니메이션이벤트 컴포넌트가 다 제각각이다.
- 이를 하나하나 게임오브젝트를 새로 생성하고, 인스펙터에서 컴포넌트를 하나하나 추가하기에는 시간이 오래 걸리고, 귀찮기도 하다. 이를 편하게 사용하기 위해 어떤 방법이 있을까 고민하다 생각난 방법이 커스텀 에디터다.
에셋 스토어에서 구매한 에셋을 살펴보는 중 인스펙터에 세팅된 프리셋 버튼을 클릭하면 해당 게임오브젝트가 선택한 프리셋으로 바뀌는 기능을 보고 GPT의 힘을 빌려 행동트리와 노드에 커스텀 에디터를 적용하여 버튼 클릭으로 노드를 추가하거나 노드를 변경하고, 행동트리에 세팅하는 기능을 구현했다.
- 몬스터 행동트리에 대하여 커스텀에디터를 사용한 이후 작업 효율이 2배 이상 늘어나 용도에 맞는 커스텀 에디터를 점점 더 늘려가고 있다.