게임 캐릭터의 기능을 효과적으로 관리하고 구현할 수 있도록 좋은 설계를 가지는 것은 중요하다고 생각한다. 캐릭터는 한 번에 하나의 상태를 가지므로 FSM을 구현하기로 했다.
왜 FSM인가?
FSM을 사용한다면, Behavior Tree(이하 BT)도 사용을 고려해 볼 수 있었다. Player 혹은 Enemy에 FSM을 적용하는 이유는 다음과 같다.
BT를 구현해본 적이 없다.
자랑할만한 이야기는 아니지만 BT을 직접 구현해본 적이 없다. 작년도에 졸업 작품을 진행하면서 BT에 대해서 이야기를 많이 들어봤지만, 당시 FSM을 이미 구현한 상태였어서 조금 찾아보기만 했었다.
굳이 사용할 필요가 없다.
새로운 기술을 배우는 것을 배척하는 사람은 아니다. 실제로 BT의 경우에는 추후 보스 몬스터를 구현할 때 사용하려고 마음먹고 있다. Player 객체나 보스 몬스터를 제외한 몬스터 객체에서 FSM을 사용하는 것은 불필요하다는 생각이 들었다.
유저의 컨트롤로 움직이는 Player 객체에서의 BT 사용이 불필요하다고 판단했고 액션의 가짓수가 많지 않은 몬스터 객체 AI 구현에 사용하는 것도 과하다고 판단했기 때문이다.
FSM 프레임워크의 설계
설계의 모티브는 전년도 졸업 작품에서 사용한 FSM 프레임워크의 아이디어를 가져왔다. 지인이 구현한 프레임워크를 사용했는데 Reflection을 이용한 점이 인상 깊었다. 당시에는 Reflection에 대한 이해도가 없어서 코드의 작동 원리를 이해하는 것에 그쳤는데, 졸업 작품을 진행하면서, CLR 공부를 하면서 늘어난 지식 덕분에 다룰 수 있으리라 생각했다.
해당 프레임워크를 설계하면서 생각했던 것은 성능 보다는 작년에 사용한 프레임워크에서 아쉬운 점을 개선하는 것을 중요하게 생각했다.
1. FSM은 State를 실행하는 Player와 State의 성격을 가진 Source로 구분하고 명칭은 다음과 같다.
- Player : FSMPlayer
- Source(State) : FSMSource
2. 사용 방법은 다음과 같이 처리한다.
// Player
public class ExampleFSMPlayer : FSMPlayer<ExampleFSMPlayer> {}
// Source
public class ExampleFSM_RUN_Source : FSMSource<ExampleFSMPlayer> {}
FSM Player 구현
public class ExampleFSMPlayer : FSMPlayer<ExampleFSMPlayer>
{
...
...
var sourceDatas = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.IsSubclassOf(typeof(FSMSource<T>)));
...
...
}
어려운 부분은 없었다. Assembly 클래스를 통해서 현재 실행 중인 Assembly에 접근하고 그중에서 Type 데이터를 가지고 있는 배열에 접근해서 FSMSource<T>를 상속하고 있는 Type을 가져온다.
FSMSource<T>를 상속하는 Script들은 Monobehaviour를 상속받지 않았기 때문에 Instance의 생성을 FSM Player에서 처리한다. Activator 클래스의 CreateInstance를 통해서 처리하면 된다.
Activator 클래스의 CreateInstance 메서드는 오버로딩된 메서드가 있기 때문에 원하는 인자를 위해서 명명된 매개변수를 통해서 Type과 params object[]을 매개로 하는 메서드를 불러왔다.
params object[]에는 선택된 Type의 생성자가 요구하는 매개변수를 인자로 넣을 수 있으므로, 실행하고 있는 FSM Player의 메모리 주소를 넘겨서 FSMSource에서 관리하고 있는 주체를 알 수 있도록 했다.
FSM Source 구현
public class ExampleFSM_RUN_Source : FSMSource<ExampleFSMPlayer>
{
public ExampleFSM_RUN_Source(ExampleFSMPlayer root) : base(root)
{
key = (int)ExampleFSMState.RUN;
}
}
다음은 FSM Source를 구현하는 것이다. Monobehaviour를 상속받지 않는 특성을 가지고 있으며, owner를 직접 지정하는 것이 아니라 Instance 생성 시, 전달받은 Instance로 설정하기 때문에 생성자를 열어주는 것 말고는 할 일이 없다.
단, FSM에서 State는 Key를 기반으로 Transition이 되기 때문에 State 생성 시, Key를 입력할 수 있도록 했다. 지금 코드를 보면 단점이 key 변수에 enum 값을 대입할 때, Boxing이 일어나는 것이다. Boxing 없이 값을 입력할 수 있는 방법이 있으니까 조만간 처리해야 하는 문제다.
'활동 > 게임제작동아리 브릿지' 카테고리의 다른 글
대학생 연합 게임 제작 동아리 브릿지(BRIDGE) 12기 후기 (0) | 2024.07.11 |
---|---|
[Project Talisman] #4. Status 시스템 개선하기 (0) | 2024.06.19 |
[Project Talisman] #3. Data Converter 리팩터링 (0) | 2024.04.22 |
[Project Talisman] #1. Data Converter 구현하기 (1) | 2024.04.19 |