[Unity] State Pattern
State Pattern
위의 사진은 애니메이션 상태 전이를 위한 Animator임을 이미 유니티 사용자들은 알고있을 것이다. 흔히 이를 유한 상태 머신(FSM)이라고 하는데, 상태 패턴을 기반으로 한다.
State는 오브젝트의 다양한 상태를 제어한다. 주로 게임에서의 상태 패턴은 애니메이션이나 NPC에 적용된다. 유니티에서는 이러한 상태 패턴을 시스템화되어있다.
상태 패턴은 객체 내부 상태에 따라 스스로 행동을 변경하는 것이다. 상태를 클래스로 캡슐화하고, 상태 전환을 클래스 간의 참조 변경으로 처리한다. 상태 패턴의 예시는 캐릭터 애니메이션의 상태, 네트워크 연결 상태, 툴 상태 등이 존재한다.
- 장점
상태를 캡슐화해 상태 전환 로직을 각 상태 클래스에 분리 가능 → 유연한 상태 전환
상태 전환을 더 쉽게 관리하고 확장
상태 전환 로직이 각 상태 클래스에 분리되어 있어 코드의 가독성 향상
상태를 추가 및 변경 시 다른 상태 클래스의 영향 없이 독립적으로 작업 → 유지보수 용이
- 단점
상태별로 별도의 클래스가 필요하므로 클래스 수 증가
상태가 많아질수록 관리해야 할 클래스와 코드의 복잡성 증가
이러한 단점들도 존재하므로, 굳이 모든 상황에서 디자인 패턴을 적용할 필요 없이 필요한 부분이라고 생각될 때 적용하면 된다.
유한 상태 기계(FSM, Finite State Machine)
주로 게임에서는 상태 패턴을 FSM으로 확장해서 사용한다. FSM은 시스템이 가질 수 있는 모든 상태와 한 상태에서 다른 상태로 전환하는 규칙을 정의하는 모델이다.
- FSM의 요소
- 상태(State) : 시스템이 가질 수 있는 각각의 상태
- 전환(Transition) : 한 상태에서 다른 상태로의 전환 규칙
- 이벤트(Event) : 상태 전환을 일으키는 외부 입력이나 조건
- 동작(Action) : 상태 전환 시 또는 트정 상태에 있을 때 수행되는 행동
만약 FSM을 단순히 코드로 만들게 된다면 아래처럼 나오게 될 것이다.
public class UnrefactoredPlayerController : MonoBehaviour {
private PlayerControllerState state;
private void Update() {
GetInput();
switch(state) {
// 각각의 로직을 구현해줘야 함
case PlayerControllerState.Idle:
Idle();
break;
case PlayerControllerState.Walk:
Walk();
break;
...
}
}
private void GetInput() { ... }
private void Walk() { ... }
...
}
굉장히 간단한 FSM 상태이지만, 이렇게 만들게 되면 확장하기 불편해진다. 보통은 이런식으로 만들지 않고 State Interface를 따로 분리해서 사용한다. 아래의 예시는 단순히 예시이며, 모든 상황에서 이렇게 구현하는 것이 아니므로 상황에 따라 결정하면 된다.
public interface IState {
public void Enter() { ... }
// 상태 진입
public void Update() { ... }
// 매 프레임마다 실행, 상태 업데이트 및 전환 조건 확인
public void Exit() { ... }
// 상태에서 탈출
}
인터페이스를 구체화시킨 코드를 살펴보자. 아래의 코드는 Idle상태를 하나로 예를 든다.
public class IdleState : IState {
private PlayerController player; // controller 참조
public IdleState(PlayerController player) { this.player = plaer; }
public void Update() {
// transition 조건 확인
if(!player.IsGrounded) { // 땅에 서있는 상태가 아닐 때
player.PlayerStateMachine.TransitionTo(player.PlayerStateMachine.jumpState);
// jump state로 변경
}
// x축이나 z축이 움직일 때
if (Mathf.Abs(player.CharController.velocity.x) > 0.1f ||
Mathf.Abs(player.CharController.velocity.z) > 0.1f) {
player.PlayerStateMachine.TransitionTo(player.PlayerStateMachine.walkState);
// walk state로 변경
}
}
}
Idle 상태에서는 전환 조건이 두 가지 존재한다. 위처럼 점프 상태나 걷는 상태로 변경해주면 된다. Idle말고 Jump나 Walk에서도 동일하게 각각의 전이 조건을 따로 클래스로 생성해서 코딩해주면 된다. 한 클래스에 묶여지지 않고 각각의 클래스를 두므로 유지보수에 용이해진다.
[Serializable]
public class StateMachine {
// State 들을 통합해서 관리해줌
public IState CurrentState { get; private set; }
public WalkState walkState;
public JumpState jumpState;
public IdleState idleState
// 상태를 지니고 있고, 상태들에게 플레이어를 참조해줘야 하므로 연결시켜줌
public StateMachine(PlayerController player) {
this.walkState = new WalkState(player);
this.jumpState = new JumpState(player);
this.idleState = new IdleState(player);
}
public void Initialize(IState startingState) {
CurrentState = startingState; // 처음 상태를 지님
startingState.Enter();
}
public void TransitionTo(IState nextState) {
CurrentState.Exit();
CurrentState = nextState;
nextState.Enter();
}
public void Update() {
if(CurrentState != null)
CurrentState.Update();
}
}
각각의 상태를 구현하고 상태 머신 클래스를 구현해보면 위와 같이 상태를 추가해주고, 참조해주며 상태를 변경만 해주면 된다.
State Machine Behaviour
유니티에서 FSM을 제공하긴 하지만 이를 코드로 다 맞추기에는 부족한 점이 존재한다. 그래서 유니티에서는 State Machine Behaviour 를 제공해준다. FSM을 기반으로 상태를 전환하는 등 여러 기능을 확장하고 구현할 수 있다.
참고로 이 State Machine Behaviour는 Scriptable Object를 상속받으므로 MonoBehaviour가 붙는 것이 아니다보니 애니메이터 메카닉 시스템에 붙게 된다. SMB는 MonoBehaviour를 사용할 수 있도록 확장하는 제네릭 클래스가 존재한다.
SMB에 대해 조금 더 이해하고 싶으면 예제에서 다룬 3D Game Kit Lite 프로젝트를 살펴보면 된다.