본문 바로가기

Unity/Study

Unity C#) 디자인 패턴 가이드 #6. 스테이트 패턴(State pattern)

본 글은 유니티에서 발간한 전자책 'Level up your programming with game programming patterns'을 정리한 글입니다. 원문은 다음 링크에서 확인하실 수 있습니다.

https://unity.com/kr/resources/level-up-your-code-with-game-programming-patterns

 

 

1. States and state machines

 

조작 가능한 캐릭터를 구축하는 것을 상상해보자. 평소에는 캐릭터가 땅에 붙어있다가 컨트롤러를 조작하면 걷거나 달릴 것이고, 점프 버튼을 누르면 캐릭터가 공중으로 뛰었다가 이후 착지하여 기본 상태로 돌아갈 것이다.

 

이처럼 게임은 상호작용적이며, 런타임동안 바뀌는 여러 시스템들을 추적하는 것이 필요하다. 위에서 언급한 캐릭터의 다양한 상태를 나타내는 다이어그램은 다음과 같이 나타낼 수 있을 것이다.

상태(state) 다이어그램

 

이 다이어그램은 플로우차트와 닮았지만 다음의 특징을 가진다.

- 여러 상태(이하 state)를 포함하며, 동시에 여러 state가 활성화된 상태로 있지 않고 한순간에 하나의 state만 활성화되어 있다.

- 각 state는 조건에 따라 다른 state로의 변이(이하 transition)을 발생시킬 수 있다.

 

위 다이어그램은 Finite-State Machine(FSM)이라고 불리는 것의 일종이다. 게임 개발에서 일반적인 사용 사례 중 하나는 게임 캐릭터나 다른 오브젝트의 내부 state를 추적하는 것이다.

 

기본적인 FSM을 구현하기 위해 다음과 같이 단순하게 enum과 switch 문을 사용할 수 있다.

 

이 스크립트는 FSM으로써 작동하긴 할 테지만, 새로운 state나 관련된 로직을 더 추가하기 위해 스크립트를 계속 수정해야 하므로 코드가 금방 지저분해질 것이다.

 

 

2. Example: Simple state pattern

 

state 패턴으로 로직을 재정비하여 위 문제를 해결할 수 있다. state 패턴은 다음 두가지 사항을 만족시킬 수 있다.

- 객체는 내부 state에 따라 동작을 바꾼다.

- state-specific한 동작이 독립적으로 정의되어 새로운 state를 추가하는 것이 기존의 state의 동작에 영향을 미치지 않는다.

 

위 예시의 UnrefactoredPlayerController는 state에 따라 동작이 변하지만, 두번째 사항을 만족하진 못해 코드가 지저분해진다. state 패턴은 다음과 같이 state를 한 객체로써 캡슐화하여 문제를 해결한다.

 

캡슐화된 state 객체의 구조

 

state에 들어가게 되면, 해당 state를 빠져나가는 특정 조건을 달성하기 전까지 루프를 맴돌게 되는 구조이다. 이를 다음과 같이 인터페이스를 사용하여 구현할 수 있다.

 

게임의 모든 state에 IState 인터페이스를 구현하여 위 구조 상의 Entry, Update, Exit 로직을 구현할 수 있다. 샘플 프로젝트에선 IState를 구현하는 클래스로 WalkState, IdleState, JumpState의 state들을 생성하였다.

https://github.com/Unity-Technologies/game-programming-patterns-demo/tree/main/Assets/10%20State/Scripts/Pattern/States

 

이렇게 구현한 state에 들어가고 나가는 플로우를 관리하는 클래스 StateMachine을 정의해보자. 언급한 3개의 state들을 사용하는 경우 StateMachine은 다음과 같을 것이다.

 

StateMachine은 각 state를 관리하기 위해 그에 따른 public 객체를 참조한다. 이때 StateMachine은 MonoBehaviour을 상속받지 않으므로,  각 인스턴스를 준비하기 위해 다음과 같이 생성자를 이용한다.

 

생성자에 셋업에 필요한 파라미터를 전달하면 된다. 예시의 각 state에선 PlayerController가 이용되어 이를 전달하였다.

 

각 state 객체가 고유한 로직을 가지므로, state를 추가하는 것이 기존의 state에 영향을 미치지 않는다. 다음은 state 중 IdleState의 예시이다.

 

이때도 역시 생성자에 필요한 파라미터를 넘겨준다. 예시에선 player(PlayerController)가 이미 StateMachine과 그외 Update에 필요한 것들을 참조하고 있으므로 player만 생성자에 넘겨주었다. IdleState의 경우 Update에서 player가 참조하는 CharacterController의 속도나 JumpState를 검사하며 StateMachine의 TrainsitionTo 메서드를 적절하게 호출한다.

 

 

3. Pros and cons

 

state 패턴을 통해 SOLID 원칙을 잘 준수할 수 있다. 각 state 스크립트는 비교적 짧으며, 그저 다른 state로의 transition 조건을 검사할 뿐이다. 또한 더 많은 state를 추가하는 것이 기존 state들에 영향을 미치지 않아 불필요한 switch나 if문을 피할 수 있다.

 

반면에 필요한 state가 많지 않다면 패턴을 도입하는 것이 과한 것일 수 있다. state 패턴은 state들이 어느 정도 수준으로 확장되는 것으로 기대될 때 의미가 있을 수 있다.

 

4. Improvements

 

샘플 프로젝트에선 플레이어의 내부 state에 따라 캐릭터의 색깔과 UI가 업데이트되도록 하였다. 실제 게임을 만들 때는 state에 따른 훨씬 더 복잡한 효과가 있을 수 있다.

 

- animation과 state 패턴을 결합할 수 있다.

state 패턴의 일반적인 적용 예 중 하나는 animation이다. Unity의 Animator 창을 사용해봤다면, 그 작업의 흐름이 state 패턴과 잘 매치된다는 것을 알 수 있을 것이다. 각 애니메이션 클립이 한 state를 나타내며 한번에 한 state만 활성화된다.

 

- event를 추가할 수 있다.

외부 객체에 state 변화를 알리기 위해 event를 추가하는 것이 좋을 수 있다(옵저버 패턴 참조). state에 진입하거나 나갈 때 event를 설정하여 관련 리스너에게 알려 런타임 중에 응답하도록 할 수 있다.

 

- 계층 구조(hierachy)를 추가할 수 있다.

state 패턴으로 더 복잡한 개체들을 관리하기 시작하면, 계층적인 state machine을 구현하고 싶어질 수 있다. 불가피하게 state가 유사할 때, 상위 state를 정의하고 공통적인 동작을 포함시키면 될 것이다. 가령 플레이어나 땅에 붙어있는 상태이면, WalkingState나 RunningState 둘 중 어느 state에 있든 상관없이 웅크리거나 점프할 수 있을 것인데, 이 경우 GroundedState를 선언하고 WalkingState와 RunningState가 이를 상속받게 할 수 있을 것이다.

 

- 간단한 AI를 구현할 수 있다.

Finite-state machine은 기본적인 적 AI를 만들 때 도움이 될 수 있다. 가령 다음과 같이 NPC의 행동 패턴을 FSM 식으로 설계할 수 있다.