본문 바로가기

내배켐 Unity TIL

Unity 56일차 TIL - Unity FSM(디자인 패턴) 트러블슈팅

FSM으로 플레이어 이동 및 공격 및 방어를 구현해보면서 문제점이 하나 생겼다.

예시

이런식으로 공격 키를 누른 상태에서 이동하거나 Idle 상태에서 공격 키를 계속 누른 상태를 구현하고 싶었는데 Action에서 누른 상태를 인식해서 State를 어떻게 넘겨줘야 될지가 고민이였다.

 

기존에 Performed가 누른 상태일 때 매번 호출되는 이벤트인 줄 알았었는데 막상 Debug를 찍어보니 1번씩 밖에 호출이 안됐다.

 

그래서 어떻게하면 각각의 상태에서 공격이나 방어 키를 누르고 있는 상태라는 걸 인식할 수 있고 또 그걸 어떻게 넘겨줘야 될지가 고민이였다.

 

처음에는 State로 사용하는 Class들이 MonoBehavior를 상속받지 않기 때문에 MonoBehavior를 상속 받는 Player Class 쪽에서 Coroutine을 활용할 수 있지 않을까 싶었다.

그래서 각 State 별로 상속받는 BaseState에서 protected bool 변수를 만들어서 공격 혹은 방어 여부를 확인하고 true가 됐을 때 Player 쪽에서 만들어둔 Coroutine을 돌려서 일정 시간 뒤 false로 변수를 바꿔주는 식으로 제어하려고 했었다.

 

어찌저찌 하기는 했지만 뭔가 찝찝한 기분이 들어서 더 나은 방법이 있을지 고민을 해보다가

public interface IState
{
    public void Enter();
    public void Exit();
    public void HandleInput();
    public void Update();
    public void PhysicsUpdate();
}
public abstract class SStateMachine
{
    public abstract void ChangeState(IState state);

    public abstract void HandleInput();

    public abstract void Update();

    public abstract void PhysicsUpdate();
}

public class PlayerStateMachine : SStateMachine
{
	protected IState playerCurrentState;
    
    public override void ChangeState(IState state)
    {
        playerCurrentState?.Exit();
        playerCurrentState = state;
        playerCurrentState?.Enter();
    }

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

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

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

public class PlayerBaseState : IState
{
    public virtual void Enter()
    {
    
    }

    public virtual void Exit()
    {
    
    }

    public virtual void HandleInput()
    {
    
    }

    public virtual void PhysicsUpdate()
    {
    
    }

    public virtual void Update()
    {
        
    }
}

각각의 State들은 PlayerBaseState를 상속 받는다.

위 코드를 보면 알 수 있듯이 ChangeState에서 매개변수 IState를 받아와서 기존에 State가 있다면  Exit 해주고, PlayerStateMachine에 있는 IState 변수를 매개변수 값으로 바꿔준 뒤 Enter 함수가 있다면 실행시킨다.

 

그리고 이 PlayerStateMachine의 HandleInput과 Update 함수는 Player Update문에서 실행하고, physicsUpdate문은 FixedUpdate 문에서 실행해준다.

PlayerBaseState는 IState라는 Interface를 상속받았기에 Interface에 있는 함수들을 이후에 상속받는 State들에서 override 해줘서 사용할 수 있게 하기 위해서 virtual로 구현해준다.

 

PlayerStateMachine에서 생성자가 만들어 줄 때 각각의 State들을 생성자로 만들어주면서 매개변수로 PlayerStateMachine을 넣어주면 각각의 Class들은 Istate에 있는 함수들을 상속받게 될 수 있다.

 

그리고 각 상태들이 변할 때 ChangeState 함수를 활용해서 PlayerStateMachine에서 만들어둔 각 State 별 변수들을 활용해서 PlayerStateMachine에 있는 State를 playerCurrentState를 변경해준다.

 

그러니까 결론적으로 각각의 State들에서는 항상 Update문을 사용할 수 있게되는데, 그래서 delayTime을 float 변수로 만들고 Time.deltaTime 만큼 Update 문에서 감소를 시켜주면서 bool 변수를 관리하면 어떨까란 생각을 해봤다.

 

각각의 State들이 상속받는 PlayerBaseState에서 각 상태 별로 bool 변수를 protected 유형으로 만들어주고, 각 상태에 들어갔을 때 알맞은 bool 변수를 true로 변환시켜주고 Update 문에서 delayTime 만큼 감소를 시켜줘서 delayTime <= 0 이 됐을 때 bool 변수를 false로 변환해주면서 Action.IsPressed()를 활용해서 해당 키가 눌려있는 중인지 여부를 판단하고 상황에 맞게 State들을 변환해주는 식으로 구현할 수 있었다.

 

구현

Run State 전에 WalkState를 거치는 이유는 움직이는 도중에 가속도를 주는 느낌으로 구현하고 싶어서 WalkState에서 RunState로 넘어갈 수 있게끔 구현했기 때문이다. 달리는 키와 이동 키를 동시에 누르게 됐을 때 가끔 먹히지 않는 현상이 있어서 WalkState에 들어갔을 때 Enter함수 쪽에서 Run Action이 IsPressed()인지 확인하고 true라면 바로 RunState로 넘겨준다. false라면 WalkState 로 진입한다.

 

FSM을 구현해보면서 객체지향적인 프로그래밍이 뭔지 대강 감은 잡히지만 아직 어려운 것 같다.

막상 구현해보고나니 보기도 편하고 유지보수가 하기에는 용이할 것 같지만 이전에 배웠던 SOLID 원칙을 다 지키지 못한다는게 아쉬운 것 같다.