게임을 즐기다보면 금방 익숙해지는 흔히 '커맨드'라 불리는 것이 있다. WASD 또는 화살표키로 전후좌우 움직임을 조절하거나,

shift키로 걷기/뛰기 전환 내지는 웅크리기가 된다던가,

f키로 탈 것에 타거나 내리기가 되고,

장애물 앞에서 space키를 누르면 장애물을 뛰어 넘거나 엄폐 동작을 취하기도 한다.

그 밖에도 게임마다 필요한 행동의 커맨드들이 있다. 이런 커맨드는 어떤 조건에 따라 어떤 방법으로 제어되고 있을까? 또 게임을 계속 하다보면 특정 커맨드는 특정 조건이 만족되어야만 실행이 가능하며, 연계되는 커맨드가 있는 반면, 특정 커맨드를 통해 이전 동작을 중간에 취소(모션 캔슬)할 수도 있다.

1. 마구잡이 개발

교육용 프로그래밍 언어 "엔트리"로 수업에 예제로 활용하기 위한 RPG를 개발하고 있다. 아바타 스프라이트는 무료로 배포된 이미지를 어떻게 구해왔고 이제 커맨드에 따라 움직이도록 하기만 하면 된다. 이게 되게 간단한 일이라고 생각하고 코드를 짜봤지만 막상 실행해보니 고려해야 할 점이 꽤 많다.
가장 먼저 마주한 문제는 "커맨드 충돌"이다. 왼쪽으로 걷기를 시작하면 왼쪽으로 걷게 할 수 있다. 오른쪽으로도 마찬가지다. 공격도 역시 구현할 수 있다. 단일 동작을 실행하도록 하는 것은 정말 쉽다. 그 동작에 필요한 프레임만 순차적으로 재생하면 된다. 하지만 여기서 왼쪽으로 걷던 도중 오른쪽으로 걷게 한다면 어떻게 될까? 오브젝트는 왼쪽으로 걷는 모션과 오른쪽으로 걷는 모션을 동시에 실행하기 시작한다. 프레임은 꼬여서 빠르게 번쩍거리고 움직임은 중첩되어 제자리에 깜빡거리기만 한다. 그리고 왼쪽으로 걷는 모션이 먼저 끝나면 어색하게 오른쪽으로 걷기가 이어진다. 걷는 도중 공격도, 점프도 마찬가지다. 모든 커맨드는 독립적으로 구현하기 너무 쉽지만, 어떤 동작에 '이어서' 다른 동작을 수행하도록 하는 것이 쉽지 않다.
심지어 커맨드 충돌에는 똑같은 커맨드끼리도 충돌된다는 문제가 있다.

왼쪽으로 움직이는 모션이 끝나기 전에 또 왼쪽 키 입력이 이루어진다면 두 커맨드가 동시에 실행되면서 역시 프레임이 꼬인다.
키 다운도 문제가 된다. 키보드에는 보통 키 다운을 지속하면 키 입력을 연속하는 기능이 있다.

그래서 일정 시간 이상 키 다운을 지속하는 경우, 무수히 많은 커맨드가 중복되는 문제가 발생한다. 프레임이 꼬이는 것은 둘째치고 오브젝트의 이동이 등가속도 운동이 되며 빠르게 화면 밖으로 사라지는 모습도 볼 수 있다.
2. State를 활용한 커맨드 충돌 해결
커맨드 충돌을 해결하기 위해서는 커맨드가 입력되는 조건을 단순히 "키 다운"만으로 판정해선 안된다. 여기서 도입해야 할 개념이 바로 "상태(state)"다. 상태는 말 그대로 오브젝트가 현재 수행 중인 동작을 나타내며 이 정보를 토대로 특정 커맨드 입력을 받을 것인지, 말 것인지 결정할 수 있게 된다. 앞서 문제가 되었던 중복 커맨드 역시 이미 최초 커맨드가 입력되어 상태가 변경되었다면, 이후의 같은 커맨드들은 무시될 수 있다.

이렇게 하면 모션이 실행되는 도중에 다른 커맨드가 입력되는 것을 막을 수 있다. 이렇게 커맨드간 충돌은 간단하게 해결되었다. 하지만 여전히 남아있는 문제가 있다. 왼쪽으로 걷는 도중, 오른쪽으로 걷기 커맨드를 입력한다면 보통 게임에서는 곧바로 오른쪽으로 방향을 전환할 수 있다. 하지만 이 상태에서는 왼쪽 걷기 모션이 끝나기 전까지 오른쪽 걷기 커맨드는 입력될 수 없다. 즉시 방향전환이 어렵다는 뜻이다. 이렇게 되면 몇 가지 문제가 더 발생하는데, 보통 달리기 모션은 걷는 도중 shift키 다운을 하는 것으로 표준화가 되어있다. 그런데 중복 커맨드가 이렇게 모두 무시된다면 달리기 커맨드는 실현조차 불가능하게 된다. 여기서 상태 간의 관계 정의가 필요하다. 그리고 이를 통해 커맨드 간의 연계를 특정 상태가 어떤 상태로 이어질 수 있는지로 정리할 수 있다.
3. Finite State Machine
RPG 구현에서 이런 걸 만나게 될 줄 몰랐다. 하지만 구현을 하다보니 자연스럽게 떠오르는 모든 아이디어가 FSM을 가리키게 되었고 구글링을 해보니 업계에서도 RPG 엔진을 만들 때 FSM이 필수적으로 요구된다는 걸 알게 되었다. 이런... 이제 좀 코딩 짬이 찼나 싶은...

먼저 아바타의 상태를 정의해보자. 아무것도 하지 않고 있는 대기 상태를 idle, 걷는 상태를 walk, 달리는 상태를 run으로 정의했다. 이 세 상태는 조건만 맞으면 커맨드 입력에 따라 곧바로 모션이 시작되어야 한다. 가령 걷는 모션이 총 12프레임으로 되어있다고 해보자. 초당 60프레임의 게임에서 걷는 모션은 0.2초마다 끝나게 된다. 만약 0.5초 동안 걷다가 달리기를 시작한다면 모션이 2번 반복된 후, 3번째 모션이 진행 될 때 달리기 커맨드가 들어왔을 것이다. 여기서 두 가지 반응을 생각할 수 있다. 먼저 모션을 마저 재생하고 모션이 끝났을 때 다음 커맨드를 실행하는 것이다. 이렇게 구현한 게임이 있다면 아마 조작감 면에서 혹평을 맞고 있을 거다. 지금까지 태어나서 즐겨본 모든 게임에 이런 식의 커맨드 처리는 본적이 없다. 걷기, 달리기 모션 처리의 표준은 현재 재생 중인 모션을 즉시 중단하고 새로운 커맨드의 모션을 실행하는 것이다. 즉, 걷기 커맨드에 따라 실행되고 있던 프레임 처리가 즉시 중단되고, 달리기 모션을 위한 프레임 처리가 시작되어야 한다는 것이다.
이제 점프를 jump로 정의해보자. 이 동작은 조금 더 복잡하다. 점프는 idle, walk, run 상태의 어느 시점에서도 즉시 실행 가능하지만 점프를 실행하고 있을 때는 idle, walk, run 모두 즉시 실행할 수 없다. 점프는 모션이 완전히 끝난 이후에 이어지는 커맨드를 실행할 수 있다. 따져보면 jump 상태는 idle, walk, run 상태보다 상위 상태라고 말할 수 있다.
attack 상태를 보자. 총 4번으로 연계되는 공격이다. 이 중 attack 2는 이동 중인 자세에서 곧바로 연계가 가능한 공격이다.(화살표 하나 빠졌다. run 상태에서도 attack 2 상태로 이어질 수 있다. 모든 공격은 1, 2, 3, 4 순으로 이루어지며 각 공격 이후 후 딜레이가 주어진다. 이 후 딜레이 시간 내에 다음 커맨드가 입력된다면 다음 모션으로 공격한다. 이 상태는 종료 시점의 커맨드 입력 여부에 따라 다음 상태가 결정된다.
guard, parry, block 상태는 각각 가드 상태, 패리 상태, 피격 방어 상태다. 가드 커맨드를 입력하면 jump 모션 중이 아닌 경우, 곧바로 guard 상태로 바뀌며 가드 모션이 시작된다. 이 모션에도 후 딜레이가 주어진다. 후 딜레이 시간 내에 피격이 발생하면 parry 상태로 변경되며 다음 커맨드로 이어질 수 있다. 반면 후 딜레이 이후에도 가드 커맨드가 계속 이어지고 있다면 guard 상태가 유지되고 이 시점에서 피격이 발생하면 block 상태로 변경되며 방어율에 따라 감소된 피해량 만큼 체력이 감소하고 피격 모션으로 이어진다.(그림 상에는 피격 상태가 빠졌다.)
이 상태들의 연결 관계를 FSM이라고 생각하고 처리해버리면 상태 변환의 구현은 끝난다. 문제는 모션 처리다.
4. 코루틴(Co-routine)으로 모션 처리

이렇게 아바타의 상태 관리는 중첩적으로 관리되어야 할 요소가 있기도 하고, 상태와 상태 간의 연계가 매우 중요하다. 그리고 특정 조건에 따라 모션이 즉시 중단되었다가 다른 모션이 끝난 후에 재개되기도 해야 한다. 이런 복잡한 프레임 관리를 단일 루틴으로 관리할 수 있을까? 아직 코딩 초보인 나는 잘 모르겠다. 이 문제를 간단하게 해결 시켜줄 친구가 바로 코루틴이다.
전지적 나무위키에 따르면 코루틴은 다음과 같은 특징을 갖는다.
- 중단과 재개: 코루틴은 실행 중간에 특정 키워드를 사용하여 실행을 중단하고 상태를 저장한 뒤, 나중에 다시 실행을 재개할 수 있다.
- 비동기 처리: 입출력(I/O) 작업이나 긴 실행 시간을 요구하는 작업에서 비동기적으로 실행되며, 동시성을 지원한다.
- 가벼움: 코루틴은 운영체제 스레드와 달리 가벼운 단위로 동작하며, 하나의 스레드에서 다수의 코루틴을 실행할 수 있다.
- 상태 유지: 코루틴은 중단된 시점의 상태(변수, 호출 스택)를 유지하며, 재개 시 이전 상태를 복원하여 실행을 이어간다.
말만 들어도 복잡한 모션 관리에 아주 적합할 것 같다.
하지만 정말 안타깝게도 엔트리에서는 코루틴이 지원되지 않는다는 점... 이걸 극복할 수 있는 새로운 방법을 찾아 다음 글로 정리해보겠다.......