본문 바로가기

Unity

Unity에서의 실제 FSM 구현에 대해

들어가며..

흔히 게임에서 캐릭터를 조종한다던가 할 일이 있을 때 가장 먼저 배우는 것이 유한 상태 기계, FSM이다. FSM에 대해서는 이미 매우 잘 알려져 있고 양질의 정보도 쉽게 접할 수 있으므로 이 글에서 FSM에 대한 상세한 설명은 생략한다. 

 

하지만 FSM이라는 개념에 너무 매몰되어 생각하는 경우 오버엔지니어링이 잔뜩 된 좋지 못한 코드를 쓰는 경우가 종종 있다. 특히 AI의 구현을 FSM으로 구현하려고 할 경우 그렇게 될 가능성이 큰데, 이 글에선 그에 대해 이야기한다.


"게임에서의" FSM과 실제 구현에서의 문제

그런데 사실 컴퓨터과학에서 말하는 FSM과 실제로 게임에서 일반적으로 구현하는 FSM은 약간의 차이가 있다. 컴퓨터과학에서 이야기하는 FSM이 어떤 알고리즘을 수학적으로 표현하는 모델이라면, 흔히 실제로 구현하는 FSM은 단순히 어떤 상태를 기준으로 동작을 분기시키는 것이다. 물론 후자의 경우도 FSM이라고 하면 FSM이지만, 굳이 불필요하게 어려운 개념을 끌고 올 필요는 없다.

public class FSM
{
    public int state;

    public void Update()
    {
        if (state == 1)
            Console.WriteLine("1");
        if (state == 2)
            Console.WriteLine("2");
        if (state == 3)
            Console.WriteLine("3");

        if (Input.GetKeyDown(KeyCode.A))
            state = 1;
        if (Input.GetKeyDown(KeyCode.S))
            state = 2;
        if (Input.GetKeyDown(KeyCode.D))
            state = 3;
    }
}

위의 예는 아주 간단한 FSM(?)의 예이다. 유한한 상태와 전이가 정의되었으므로 FSM이라고 해도 무방하다.

하지만 코드를 보면 알 수 있듯이, 이런 구조는 FSM이라는 개념을 끌고 오지 않고도 누구나 의식하지 않고 만들던 구조이다. 그러나 위의 방식으로 만드는 경우는 흔하지 않은데, 이처럼 if-else를 남발할 경우 읽기 어렵다던가 수정하기 어렵다던가 하는 이유들이 있기 때문이다. 그래서 FSM의 실제 구현 등을 찾아보면 일반적으로 상태 패턴으로 구현한 경우가 많은데, 막상 실제로 요구사항을 보면 상태 패턴으로 추상화하기 어려울 때가 종종 있다.

 

예를 들어 어떤 캐릭터를 FSM으로 정의하려고 해보자. 캐릭터에는 정지, 이동, 공격, 죽음의 상태가 있다고 가정한다. 그런데 어떤 상태에서든 HP가 0이 되면 죽을 수 있다. 공격 또한 죽음을 제외한 모든 상태에서 진입할 수 있다. 이런 식으로 유사한 전이 조건이 상태마다 붙는다면, 그 조건을 상태에 종속시키는 것은 별로 좋은 생각이 아닐 것이다.

FSM으로 표현한 병사 AI 예시 (이상)
FSM으로 표현한 병사 AI (현실)


해결법1 : HFSM

이런 문제는 잘 알려져 있는데, 대표적인 해결법 중 하나가 계층적 FSM, 즉 HFSM이다.

 

HFSM은 여러 상태를 하나의 상위 상태로 묶어서 꼬인 전이 관계를 좀 풀어보려는 구조이다.

HFSM에 대한 간단한 그림

 

언뜻 보면 좋은 생각인 것 같지만, 구현해야 할 사항이 실제로 이렇게 예쁘게 묶여있으리란 보장은 없다. 당장 위의 병사 AI 예시만 봐도 그다지 묶을만한 상태가 보이지 않는다. 상태 ABC에서 X로 가는 전이가 있는데, 상태 ABD에서 Y로 가는 전이가 있다면 어떻게 계층화를 해야 할까?

억지 예시라고 생각할 수도 있지만, 실제로 AI 등을 만들다 보면, 그리고 오랜 개발 기간을 거쳐 초기에 상정하지 않았던 요구사항들이 하나씩 생기게 되면 얼마든지 일어날 수 있는 일이다.


해결법2 : 전역 전이 함수

또 다른 방법은 현재 상태에 관계 없이 특정 상태로 강제로 바꾸는 로직을 추가하는 것이다.

 

이 방법은 직관적이기도 하고 복잡한 요구사항을 잘 처리할 수 있어서 실제로 많이 사용하는 방식이다. 

유니티 애니메이터의 Any State도 일종의 전역 전이 기능이다.

 

유니티 애니메이터에도 같은 기능이 있는데 바로 Any State 기능이 그것이다. 현재 상태와 관계 없이 특정 조건에서 특정 상태로 바로 전이시켜 줄 수 있다면 복잡한 전이 구조를 정리하는데 도움이 된다.

 

이 방법의 가장 큰 문제점은 가장 위 코드 예시에서 봤던것 같은 중첩된 if-else와 본질적으로 같다는 점이다. 코드에 신경을 덜 쓰는 순간 전역 전이 로직과 상태별 전이 로직이 무분별하게 뒤섞여서 코드를 읽기 어려워지게 될 위험이 크다. 그럴 바에는 복잡하게 상태 패턴 등을 사용하는 대신 차라리 위의 코드 예시처럼 아예 명시적으로 if-else를 사용하는 편이 가독성과 유지보수성이 나을 수도 있다.

 


해결법3 : 애초에 그거 꼭 써야 됨?

지금까지 언급된 부분은 모두 전이 관계가 지나치게 복잡해서 벌어지는 문제인데, 애초에 그런 경우에까지 FSM을 사용하려고 하는 것이 문제일 수 있다.

 

일반적으로 게임제작에서 FSM이 복잡해지는 가장 흔한 경우는 캐릭터의 AI를 구현하려는 경우이다. 요구되는 복잡도에 따라서는 FSM으로 의사결정을 구현하려는 것 자체가 잘못된 선택일 수도 있다. 주변 환경과 자연스럽게 상호작용하는  AI를 만들려면 FSM보다는 단순히 가중치 기반으로 하드코딩된 AI를 사용하거나 Behavior Tree 등의 다른 의사결정 알고리즘을 사용할 것을 고려해보는 것이 우선이다.

 

FSM으로 고통받지 않으려면 전이를 깔끔하게 정의할 수 있는 문제여야 한다는 점을 기억하자.


그 외에 FSM을 사용하는 예시

지금까지는 캐릭터 AI를 위주로 이야기했고, AI에 대해서는 FSM이 적합하지 않은 경우도 많다는 점을 이야기했다.

그러면 어떤 경우에 FSM을 사용하면 좋을까? 예시를 몇가지 들어 보겠다.

덧붙여, 여기서 FSM은 엄밀한 수학적인 의미의 FSM이 아니라, 일반적인 상태 기반 로직 구조 전반을 말하는 것이다.

  1. 게임 그 자체
    • 게임 자체는 일반적으로 상태로 잘 정의될 수 있다. 예를 들면 로딩 / 타이틀 / 본게임 / 크레딧 등의 구분이다..
    • 유니티 싱글 씬 위주의 개발을 한다면 씬 자체가 하나의 상태라고 볼 수도 있다.
    • 그렇지 않다면 코드에서 게임의 여러 단계를 상태로 나누어서 관리하는 것이 좋은 구조가 될 수 있다.
  2. UI 자체
    • 화면 하나를 차지하는 큰 UI는 그것 자체가 하나의 상태라고 볼 수 있다. 항상 하나씩만 나오고, 나오거나 들어갈 때 해야 하는 작업이 있기 마련이기 때문에 상태 기반 로직 구조를 적용하기 적합하다.
    • UI는 바로 이전 상태로 돌아가야 하는 경우가 많기 때문에 스택 기반 상태 관리가 적합한 경우가 있다, 다만 스택 기반으로 관리하면 아예 다른 모양의 스택으로 점프해야 하는 경우 아주 골치아픈 문제를 마주칠 가능성이 있다. 예를 들면 A->B->C 로 이동했는데, C에서 뒤로가기 하면 B가 아니라 D로 돌아가야 하는 상황이다.
    • 또한 여러 화면에 걸쳐 계속 나와있는 UI가 있는 경우도 적용을 고려해야 한다.
  3. 반응형 UI 조각
    • 모양이 바뀌지만 규칙은 단순한 UI 조각의 경우도 있다. 
    • 예를들면 스킬 쿨타임 아이콘의 경우, 쿨타임 안됨/사용가능/사용중/사용불가(죽음 등) 의 4가지 상태가 있다고 가정해 볼 수 있다. 이런 경우 전이과정이 명확하고 상태가 간단하므로 상태 기반으로 접근하는 것이 좋을 수 있다.
  4. 주변환경과의 상호작용이 단순한 오브젝트
    • 유연한 의사결정을 하지 않고 단순히 기계적인 반응을 하는 오브젝트의 경우 FSM을 사용하기에 적합하다. 
    • 예를 들어 감시카메라가 평소에는 좌우로 천천히 훑다가, 무언가 물체를 발견하면 그 물체를 추적하는 경우를 상상해 볼 수 있다.

 그래도 왜 FSM?

그래도 FSM을 써야 하는 이유를 찾자면, 노드와 전이로 이루어진 구조 덕분에 도식화가 쉽고, 프로그래머가 아닌 사람들이 작성 및 수정을 하도록 만들 수 있기 때문이다. 

 

유니티 메카님 애니메이션 시스템이 FSM으로 이루어진것도 아티스트가 작업할 수 있게 하기 위함이며, AI에서 FSM을 사용하는 것도 기획자가 AI를 직접 작성할 필요가 있다면 다른 대안이 딱히 없다. (마찬가지로 노드 기반으로 만들 수있는 BT 정도를 제외하면.)

 

 

결론

결론적으로 너무 뻔한 이야기지만, 특정한 어떤 구조를 맹목적으로 생각하기보다는 구현하려는 요구사항에 맞게 가장 적합한 코드를 유연하게 작성하는 것이 가장 중요하다.

경우에 따라서는 단순히 if-else를 중첩시키는게 상태를 예쁘게 객체지향적으로 캡슐화하는 것보다 나은 경우도 얼마든지 있을 수 있고, 애초에 FSM에 연연하지 않고 다른 방법, 심지어 하드코딩을 하는 방법이 더 나은 경우도 있을 수 있다. 눈앞에 있는 문제에 따라 지금까지 소개한 여러 방법 중 하나 혹은 그것들을 적당히 혼합하는 것이 정답일 수도 있고, 전혀 다른 더 좋은 구현이 있을 수도 있다.

 

혹시라도 어떤 경우에는 FSM을 사용해야 하고, 그 FSM은 어떻게 구현해야 한다는 식으로 경직된 사고방식을 가지고 있다면, 그걸 조금 내려놓으면 분명히 좀 더 좋은 코드를 작성할 수 있을 것이다.