본문 바로가기

Unity/3D Project

3D RPG - 플레이어 상태(FSM)

이 글을 읽기 전 FSM이 무엇인지 알고 오면 좋습니다.

 

BaseGameEntity.cs

    ...
    [HideInInspector]
    public Animator Animator;

    public virtual void Setup()
    {
        Animator = GetComponent<Animator>();
    }

    public abstract void Updated();
    ...

 

모든 씬에 존재하는 모든 Entity들이 상속받는 기반 클래스 BaseGameEntity 스크립트 입니다.

이 클래스는 엔티티들이 기본적으로 가져야 할 초기화(Setup)와 업데이트(Updated) 기능을 제공하며, 상속받는 클래스에서 구체적인 동작을 정의할 수 있도록 설계되었습니다.

 

전체 코드

https://github.com/HerFS/Unity-RPG/tree/main/Assets/_Scripts/FSM

 

StateMachine.cs

public class StateMachine<T> where T : class
{
    private T _ownerEntity;
    private StateOfPlay<T> _currentState;
    public StateOfPlay<T> CurrentState => _currentState;

    public void Setup(T owner, StateOfPlay<T> entryState)
    {
        _ownerEntity = owner;
        _currentState = null;

        ChangeState(entryState);
    }

    public void Execute()
    {
        if (_currentState != null)
        {
            _currentState.Execute(_ownerEntity);
        }
    }

    public void ChangeState(StateOfPlay<T> newState)
    {
        if (newState == null)
        {
            return;
        }

        if (_currentState != null)
        {
            _currentState.Exit(_ownerEntity);
        }

        _currentState = newState;
        _currentState.Enter(_ownerEntity);
    }
}

StateMachine 클래스는 여러 Entity들의 상태를 관리하고 제어하기 위해 *제네릭 방식으로 구현된 상태 기계(State Machine)입니다.

 

StateOfPlay.cs

public abstract class StateOfPlay<T> where T : class
{
    public abstract void Enter(T entity);

    public abstract void Execute(T entity);

    public abstract void Exit(T entity);
}

 

StateOfPlay 클래스는 Entity의 상태를 관리하기 위해 *제네릭 방식을 사용한 추상 클래스입니다.
Entity의 다양한 상태를 효과적으로 관리하기 위해 설계되었으며, 제네릭 방식을 통해 재사용성과 확장성을 높였습니다.
 
- Enter 메소드는 상태를 진입했을 때 1회 호출됩니다.
- Execute  메소드는 해당 상태를 업데이트 할 때 매 프레임 마다 호출됩니다.
- Exit 메소드는 해당 상태를 종료하는 메소드로 상태를 종료할 때 1회 호출합니다.
 
*제네릭 프로그래밍 -  프로그래밍은 데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그래밍 방식입니다.

 

PlayerEntity.cs

    ...
    public static StateOfPlay<PlayerEntity>[] States { get; private set; }
    public static StateMachine<PlayerEntity> StateMachine { get; private set; }

    public bool IsHausted { get; private set; }

    private void Awake()
    {
        Setup();
    }

    private void Update()
    {
    	...
        Updated();
    }

    public override void Setup()
    {
        base.Setup();

        States = new StateOfPlay<PlayerEntity>[6];
        StateMachine = new StateMachine<PlayerEntity>();

        States[(int)EnumTypes.PlayerState.Idle] = new PlayerOwnedStates.Idle();
        States[(int)EnumTypes.PlayerState.Talk] = new PlayerOwnedStates.Talk();
        States[(int)EnumTypes.PlayerState.Walk] = new PlayerOwnedStates.Walk();
        States[(int)EnumTypes.PlayerState.Run] = new PlayerOwnedStates.Run();
        States[(int)EnumTypes.PlayerState.Attack] = new PlayerOwnedStates.Attack();
        States[(int)EnumTypes.PlayerState.Die] = new PlayerOwnedStates.Die();

        StateMachine.Setup(this, States[(int)EnumTypes.PlayerState.Idle]);
    }

    public override void Updated()
    {
        StateMachine.Execute();
    }

    public static void ChangeState(EnumTypes.PlayerState newState)
    {
        StateMachine.ChangeState(States[(int)newState]);
    }
    ...

전체 코드

https://github.com/HerFS/Unity-RPG/blob/main/Assets/_Scripts/FSM/Player/PlayerEntity.cs

 

플레이어 Entity의 상태를 관리하는 시스템으로 State의 종류는 "Idle", "Talk", "Walk", "Run", "Attack", "Die" 6종류로

각 스탯마다 정해진 작업들을 수행합니다.

 

상태들을 표현하기 위해 열거형 형태로 정의 했습니다.

Enum 형태들을 사용하는 이유는 상태를 명확하게 표현할 수 있어 코드의 가독성을 높여 상태를 직관적으로 이해할 수 있습니다. 이렇게 Enum 형태들로 정의한 이유는 "https://herfs.tistory.com/10" 을 확인하시면 됩니다.

 

마지막으로 Player Entity 상태 종류에 대해 보여드리겠습니다.

 

PlayerOwnedStates.cs

namespace PlayerOwnedStates
{
    public class Idle : StateOfPlay<PlayerEntity>
    {
        public override void Enter(PlayerEntity entity)
        {
            entity.Animator.CrossFade(Globals.AnimationName.Idle, 0.2f);
        }

        public override void Execute(PlayerEntity entity)
        {
            if (Input.GetMouseButtonDown((int)EnumTypes.MouseButton.Left) &&
                !Cursor.visible &&
                GameManager.Instance.IsGameStart)
            {
                PlayerEntity.ChangeState(EnumTypes.PlayerState.Attack);
            }

            if (InputManager.Instance.MoveVector.magnitude != 0f)
            {
                PlayerEntity.ChangeState(EnumTypes.PlayerState.Walk);
            }
        }

        public override void Exit(PlayerEntity entity)
        {
            
        }
    }

    public class Walk : StateOfPlay<PlayerEntity>
    {
        public override void Enter(PlayerEntity entity)
        {
            entity.Animator.CrossFade(Globals.AnimationName.Walk, 0f);
        }

        public override void Execute(PlayerEntity entity)
        {
            if (Input.GetMouseButtonDown((int)EnumTypes.MouseButton.Left) &&
                !Cursor.visible &&
                GameManager.Instance.IsGameStart)
            {
                PlayerEntity.ChangeState(EnumTypes.PlayerState.Attack);
            }

            if (InputManager.Instance.MoveVector.magnitude == 0f)
            {
                PlayerEntity.ChangeState(EnumTypes.PlayerState.Idle);
            }
            else if (!(entity.IsHausted) && InputManager.Instance.IsRun)
            {
                PlayerEntity.ChangeState(EnumTypes.PlayerState.Run);
            }
        }

        public override void Exit(PlayerEntity entity)
        {

        }
    }
    ...
}

전체 코드

https://github.com/HerFS/Unity-RPG/blob/main/Assets/_Scripts/FSM/Player/PlayerOwnedStates.cs

 

Enter 메소드에서는 CrossFade 메소드를 사용해 애니메이션 상태를 변경합니다. 

Execute 메소드에서는 각자 상태에 맞는 행동을 처리합니다.