#05 문라이터 모작 - 플레이어 이동
📜진행 상황
일단 URP로 새로운 프로젝트를 생성한 뒤 가장 먼저 시작한 작업은 기존의 진행했던 부분까지 완료하는 것이였다. 기존에 IDLE, WALK상태까지 구현을 진행했었고 ROLL상태를 구현하고 있었다.
📜목표
- IDLE, WALK, ROLL 상태를 정의하고 각각에 상태에 맞는 동작을 실행한다.
- 각각의 상태에 맞는 애니메이션 출력.
📜InputSystem
플레이어가 체크해야 할 키 입력이 많아질수록 if문이 많아지고 입력을 체크하는 코드도 좋지 않았다. 따라서 기존의 InputManager가 아닌 InputSystem을 활용하여 입력 처리를 좀 더 간결하게 처리하였다.
Update()에서의 입력 체크
Update()에서 계속해서 플레이어의 입력을 체크한다고 생각했을 때, 체크해야 할 입력이 많이 없다면 그것 자체로는 큰 문제가 되지 않을 것이다. 하지만 RPG와 같은 게임 플레이 상태에서 입력해야 할 키가 많은 게임이라면 이러한 키 입력에 대한 참/거짓 유무를 Update()에서 계속해서 체크한다면 엔진의 연산량이 많아질 것이다.
그러면 어떻게 입력의 유무를 판단하는 게 좋을까?
간단한 해결 방법은 입력이 들어왔을 때 혹은 입력값이 바뀌었을 때를 체크해 입력에 대한 값을 체크해주면 된다. 유니티에서는 이러한 방식을 이벤트(event)라고 하며, 이러한 방식을 사용한 새로운 입력 시스템이 InputSystem이다.
InputSystem을 사용하게 된 이유
처음에는 InputManager를 사용해 플레이어의 입력을 받았다. IDLE, WALK 상태를 구현할 때만 해도 특별한 문제를 느끼지 못했다. 하지만 ROLL 상태를 구현하기 시작하면서 점점 if문이 복잡해질 것 같은 느낌(?)을 받았다. 그때 유니티의 새로운 입력 시스템인 InputSystem에 대해 알아봐야겠다는 생각을 했다. InputSystem은 특정 행동에 대한 키를 매핑하고 해당 키의 입력이 들어왔을 때 이벤트 형식으로 함수를 호출해 입력을 처리하는 방식이었다. 내가 원하던 방식이었다! 그래서 InputSystem에 대해 알아보고 실제 프로젝트에 적용했다.


📜Blend Tree
플레이어는 8방향으로 이동한다. 그러나 다행히도 8방향 모두 애니메이션이 존재하는 것이 아니라 4방향 이동 애니메이션을 적절하게 이용한다.
애니메이션에 대한 생각
IDLE, WALK, ROLL 상태에 대한 4방향의 애니메이션이 존재하고 이 애니메이션들을 각각의 상태에서 적절한 방향을 바라보고 행동하게 해주는 것이 주요 문제였다. 블렌드 트리를 사용하지 않고 모든 애니메이션 클립에 대해 transition을 적용하게 된다면 상태머신이 매우 복잡해 질 것 같았다. 3가지 상태에 각각의 애니메이션이 4개씩 존재하고 이들을 모두 연결해야 한다면 하나의 애니메이션 클립에 transition이 11개나 존재하게 되고 각각의 조건들을 달아줘야 한다는 것이다. 생각만 해도 끔찍하다…
블렌드 트리(Blend Tree)를 사용하자!
이러한 문제를 해결하기 위해 유니티에서는 서브 스테이트 머신(Sub-State Machine)과 블렌드 트리(Blend Tree)를 지원한다. 서브 스테이트 머신은 관련된 애니메이션을 또 다른 서브 스테이트로 묶어서 관리하는 것이고 블렌드 트리는 블렌드값을 사용해 여러 개의 애니메이션을 적절히 섞어 주는 것이다. 나는 이 중 4방향을 결정하는데 적절한 것을 블렌드 트리라고 생각했다. 이유는 간단했다. 입력값을 파라미터로 받고 그 입력값에 따라 플레이어의 방향을 정해주면 되겠다는 생각이 들었기 때문이다. 따라서 각각의 상태를 블렌드 트리로 구성하고 이에 따라 파라미터값을 이동 입력값으로 할당해 주었다.

상태 머신이 복잡하지 않고 보기 좋게 놓여졌다.
📜StateMachineBehaviour
애니메이션 문제를 해결하니 이제는 제어쪽에서 고민이 생겼다. 지금까지는 WillController라는 스크립트에서 각각의 상태에 대해 직접 제어해 주었다. 처음 IDLE, WALK 상태를 구현할 때까지만 해도 괜찮았다. 하지만 ROLL 상태가 추가된 후 입력값에 대한 조건문이 붙으면서 이번에도 어김없이 불안감에 휩싸였다. 단지 ROLL 상태만 추가됐을 뿐인데 체크해야 할 조건이 많아지고 이 조건들을 체크하기 위해 조건문이 점점 복잡해졌기 때문이다. 경험이 많지 않은 나도 이러한 코드가 있으면 버그가 발생하기 쉽다는 생각을 하게 될 정도였다. 이 문제를 해결하기 위해 검색을 해본 결과 FSM, 상태 패턴이라는 것이 있다는 것을 알게 되었다.
유한 상태 기계(FSM)
유한 상태 기계(FSM)은 유한한 상태를 가진 기계이다. 이러한 설명은 의미가 모호하지만, 유한 상태 기계의 요점은 간단하다. 객체는 유한한 상태를 가질 수 있으며, 한 번에 하나의 상태만 가질 수 있다. 입력이 객체에 전달됩니다. 각 상태에서는 입력에 따라 다음 상태로 전이(Transition)가 있다. 순수한 형태에서 상태, 입력 및 전이가 유한 상태 기계(FSM)의 전부이다. 가장 간단한 FSM은 if-then/switch-case문으로 구성된 유한 상태 기계이다. 그러나 상태가 많아질수록 복잡해지며 버그가 발생할 확률이 높아진다. 이 문제를 해결하기 위해 상태 패턴이 개발되었다.
상태 패턴(State Pattern)
상태 패턴은 객체의 상태에 따라 동작을 다르게 처리하는 디자인 패턴이다. 상태 패턴은 에이전트(게임 상에 어떠한 상태를 가질 수 있는 게임 오브젝트라고 생각하면 좋을 거 같다. 지극히 주관적인 개인의 생각)가 취할 수 있는 동작을 클래스로 구현한다. 그리고 각각의 클래스에 상태를 바꿔주는 메소드를 구현한다. 이때 메소드는 보통 상태의 진입했을 때(Enter), 상태가 진행되는 중(Update), 상태를 빠져나갈 때(Exit) 이 3가지 상태에 대한 메소드를 작성하게 된다.
StateMachineBehaviour
친절한 유니티는 이러한 상태 패턴에 대한 기본적인 스크립트를 제공해준다. 바로 StateMachineBehaviour이다. 이 스크립트는 각각의 서브 스테이트 머신이나 블렌드 트리에 컴포넌트처럼 추가할 수 있다. 나는 StateMachineBehaviour를 상속받은 플레이어의 상태 스크립트를 하나씩 생성했고 각각의 상태 스크립트에서 해당 상태에 맞는 행동을 취할 수 있게 했다. 따라서 이제 WillController 스크립트에는 입력값을 받고 해당 입력값과 플레이어의 데이터를 넘겨주는 함수만이 존재하게 되었다. 그리고 각각의 상태 스크립트에서는 해당 상태에 대한 동작만을 실행하기 때문에 버그가 발생할 확률도 확실히 줄게 되었다는 생각을 하였다.



StateMachineBehaviour를 활용해 각각의 상태에 대해 독립적으로 행동을 수행하게 하였고 더 이상 복잡한 if-then/swich-case문을 사용하지 않아도 되었다.
📜개발 중 이슈 사항
플레이어 로직 일부 무시됨
InputSystem을 처음 적용할 때, 이동 관련 로직을 모두 입력값을 받는 함수에 적용했었다. 그러나 이벤트 방식으로 동작하는 InputSystem은 입력값이 변경될 때만 호출되기 때문에 특정 상황에서는 로직이 실행되지 않았다. 예를 들어, IDLE 또는 WALK 상태에서 ROLL 상태로 전환할 때 방향키 입력을 전혀 변경하지 않은 경우에는 플레이어의 입력이 처리되지 않았다.
이 문제를 해결하기 위해 애꿎은 InputAction만 지속적으로 수정하고 있었다. 그러던 중 입력값을 받는 함수에서 실행하던 로직을 Update() 함수로 이동시키니 문제가 해결되었다. 이 문제는 처음 사용하는 InputSystem에 대한 이해 부족이라고 생각했다. 따라서 입력값만 받아들이고 동작에 대한 로직은 Update() 함수로 이동시켜 문제를 해결했다.
IDLE ⇒ ROLL 방향 정해주기
ROLL 상태에서 동작을 실행할 때 구르는 방향은 애니메이터의 파라미터로 값을 설정해주고 있었다. WALK ⇒ ROLL 일 때는 방향이 제대로 적용되었지만 IDLE일 때는 따로 파라미터값을 지정해주지 않기 때문에 마지막 입력값이 저장되어 가만히 바라보는 방향으로 구르지 않고 WALK상태일 때 방향으로 구르는 문제가 생겼다.
이 문제도 처음에는 마땅한 방법이 생각나지 않았다. 3D 게임이였다면 forward 벡터를 사용해 플레이어가 바라보는 방향을 쉽게 구할 수 있었겠지만 2D 게임에서 forward 벡터는 거의 없는거나 마찬가지기 때문이다. (Z축으로 원근법을 조절하게 된다면 의미가 있겠지만 내가 개발하는 2D TopDown뷰 게임에서는 의미가 없다고 생각한다.) 그래서 생각한 방법은 WALK 상태에서 마지막으로 받은 파라미터를 기준으로 방향을 정해주는 것이였다. 이 문제를 해결하기 위해 일단 이 게임에서 플레이어가 이동할 때 어떻게 움직이는지를 파악하는 것이 중요했다.
상하좌우 직선 방향으로 움직일 땐 문제가 되지 않지만 대각선 방향이 문제가 된다. 다행히 이 게임에서는 대각선으로 이동할 때 상하키를 기준으로 이동한다는 것이였다. 예를 들어 북동쪽으로 이동한다고 하면 오른쪽으로 걷는 애니메이션이 아닌 위쪽으로 이동하는 애니메이션이 출력된다는 것이다. 그리고 걷는 애니메이션이 끝나면 위쪽을 바라보게 된다.

이 문제는 IDLE 상태를 벗어날 때 X파라미터와 Y파라미터를 체크하여, IDLE 상태에서 바라보는 방향을 Will 스크립트(플레이어가 가질 수 있는 모든 데이터를 모아놓은 스크립트)에 저장한다. 그리고 ROLL 상태에 진입하면 IDLE 상태에서 바라보는 방향에 따라 구르는 방향 벡터를 설정해준다.
📜느낀점
플레이어의 상태를 구현하면서 게임 디자인 패턴이 얼마나 중요한지 새삼 와닿았다. 디자인 패턴을 적재적소에 잘 활용할 수 있다면 프로그램을 개발하는 과정에서 더 효율적이고 아름다운 코드를 작성할 수 있을것 같다는 생각이 들었다. 프로그램 개발 과정에서 어떠한 문제에 직면했을 때 적절한 디자인 패턴을 찾아보고 적용하는 연습도 많이 필요할 것 같다.
댓글남기기