본문 바로가기

내배켐 Unity TIL

Unity 55일차 TIL - Unity FSM(디자인 패턴)

이번에 최종 프로젝트를 진행하면서 플레이어 이동 구현을 맡아 이전에 강의에서 배웠던 FSM(Finite State Machine) 유한 상태 머신이라는 디자인 패턴으로 구현해보려고 한다.

 

FSM 개념

- FSM은 유한한 갯수의 상태들로 구성된 기계 및 패턴을 뜻한다.

- 상태와 상태 간의 전환을 기반으로 동작하는 동작 기반 시스템이다.

 

FSM의 구성 요소

- 상태 (State) : 시스템이 취할 수 있는 다양한 상태.

- 전환 조건 (Transition Condition) : 상태 간 전환을 결정하는 조건.

- 동작 (Action) : 상태에 따라 수행되는 동작 또는 로직

 

FSM의 동작 원리

- 초기 상태에서 시작하여 입력 또는 조건에 따라 상태 전환을 수행

- 상태 전환은 전환 조건을 충족할 때 발생하며, 전환 조건은 입력, 시간, 조건 등으로 결정

- 상태 전환 시 이전 상태의 종료 동작과 새로운 상태의 진입 동작이 수행

 

상태 기계를 구현하는 방법은 다양하다.

가장 간단한 방법은 case switch 문인데 유지보수에 어려움이 있고(상태가 추가 될 때마다 새로운 분기를 작성해야됨), 상태가 많아지고 조건이 복잡해진다면 코드가 지나치게 길어진다는 단점이 있다.

 

그래서 강의에서는 State Pattern을 활용한다. 기본적으로 객체 지향의 다형성을 활용하고, 상태를 명확하게 정의 및 상태 간 전환을 일관되게 관리할 수 있으며 복잡한 동작을 상태와 전환 조건으로 나누어 구현하므로 코드 유지 보수가 용이하기 때문이다.

 

예를 들어서 case switch 문으로 이동에 관련된 FSM을 구현했을 때 점프 요소를 추가하려면은 switch 문에 그 상태를 추가해줘야하는데, State Pattern으로 FSM을 구현했다면 새로운 Class를 만들고 연결 시켜주면 끝난다.

 

처음에는 State Pattern으로 구현하는게 감이 잘 안 잡혔지만 그냥 쉽게 생각하면 기존에 case switch 문으로 작성하던거를 Class로 나눠서 전환 조건이 됐을 때 옮겨간다라는 식으로 생각하고 작성해보니 감이 어느정도 잡히고 있는 것 같다.

 

플레이어 이동 구현을 FSM으로 선택한 이유는 우선 게임을 한 사이클 돌리는걸 빠르게 만드는게 목표인데, 그러다보니 플레이어의 관련된 기능들에 대해서 선택으로 빼둔 부분들이 어느정도 있어서 혹시라도 시간이 된다면 나중에 새로운 상태들을 더 편하게 확장시키고 싶어서 FSM으로 구현하게 됐다.

 

클래스 다이어그램

IState라는 interface를 만들어 상태 별로 사용할 함수들을 정의하고, SStateMachine을 abstract class로 만들어서 PlayerStateMachine에 상속 시켜준다. 강의에서는 추상 클래스로 만들어서 상속받는 class에서 구현할 수 있도록 한다고 했지만 음... 완성된 코드를 보면 굳이 왜 넣었는지 잘 모르겠다.

 

Player에서 PlayerStateMachine 클래스를 객체화시켜주면 PlayerStateMachine에서 PlayerBaseState를 상속받은 State들을 객체화 시켜줄 때 PlayerStateMachine을 변수에 넣어준다.

 

public interface IState
{
    public void Enter();
    public void Exit();
    public void HandleInput();
    public void Update();
    public void PhysicsUpdate();
}

public abstract class StateMachine
{
    protected IState currentState;

    public void ChangeState(IState state)
    {
        currentState?.Exit();
        currentState = state;
        currentState?.Enter();
    }

    public void HandleInput()
    {
        currentState?.HandleInput();
    }

    public void Update()
    {
        currentState?.Update();
    }

    public void PhysicsUpdate()
    {
        currentState?.PhysicsUpdate();
    }
}

이것이 기존에 사용하던 StateMachine.cs인데 내 생각에는 abstract로 class를 구현할거라면

 

public interface IState
{
    public void Enter();
    public void Exit();
    public void HandleInput();
    public void Update();
    public void PhysicsUpdate();
}

public abstract class StateMachine
{
    public abstract void ChangeState(IState state);

    public abstract void HandleInput();

    public abstract void Update();

    public abstract void PhysicsUpdate();
}

이렇게 바꾸고 상속받는 PlayerStateMachine에서 구현하는 것이 맞는 방식이라고 생각한다.

public class PlayerStateMachine : StateMachine
{
    protected IState currentState;
    public Player player { get; }
    public Vector2 MovementInput { get; set; }
    public float MovementSpeed { get; private set; }
    public float RotationDamping { get; private set; }
    public float MovementSpeedModifier { get; set; } = 1f;
    public float JumpForce { get; set; }
    public Transform MainCamTransform { get; set; }

    public PlayerIdleState IdleState { get; private set; }
    public PlayerWalkState WalkState { get; private set; }
    public PlayerRunState  RunState { get; private set; }
    public PlayerJumpState JumpState { get; private set; }
    public PlayerStateMachine(Player player)
    {
        this.player = player;

        MainCamTransform = Camera.main.transform;

        IdleState = new PlayerIdleState(this);
        WalkState = new PlayerWalkState(this);
        RunState = new PlayerRunState(this);
        JumpState = new PlayerJumpState(this);

        MovementSpeed = player.Data.GroundData.BaseSpeed;
        RotationDamping = player.Data.GroundData.BaseRotationDamping;
    }

    public override void ChangeState(IState state)
    {
        currentState?.Exit();
        currentState = state;
        currentState?.Enter();
    }

    public override void HandleInput()
    {
        currentState?.HandleInput();
    }

    public override void Update()
    {
        currentState?.Update();
    }

    public override void PhysicsUpdate()
    {
        currentState?.PhysicsUpdate();
    }
}

그래서 다른 사람이 StateMachine을 상속받아서 다른 StateMachine을 만들려고 할 때 강제로 구현해야 될 함수를 만들어 놓게 하는게 좋지 않을까 싶다. 만약에 abstract class에서 사용하는 함수의 내용이 다 동일하다면 virtual로 구현하면 될 것 같다.

 

그래서 결론적으로 상태별로 Class를 구현해서 PlayerStateMachine 쪽에서 생성자를 만들어주고, 필요할 때 stateMachine에 기능별 함수들을 적절하게 호출해서 관리해주면 된다.