Unity/디자인 패턴

[Unity] 상태 패턴(FSM, HFSM)

게임 프로그래머 2025. 5. 4. 19:53

FSM(Finite State Machine, 유한 상태 기계)

게임 오브젝트의 상태를 체계적으로 관리하고 전환하기 위한 설계 패턴

플레이어나 적 AI처럼 상태에 따라 행동이 달라지는 시스템에 자주 사용

 

사용하는 이유

코드의 가독성과 유지보수성 향상
→ 상태별 로직을 분리해서 각 클래스나 메서드에 구현 가능.

 

복잡한 조건 분기를 명확하게 구조화
→ if-else나 switch 남발 대신 상태 전이 중심 설계.

 

확장성과 재사용성 좋음
→ 새로운 상태 추가 및 전이 조건 설정이 쉬움.

 

IState (상태 인터페이스)

// 상태 인터페이스
public interface IState
{
    void Enter();
    void Execute();
    void Exit();
}

 

역할

모든 상태들이 따라야 할 공통 규칙을 정의

각 상태는 이 인터페이스를 구현(implements)하여, 상태 진입, 유지, 종료 시의 동작을 정의함

 

IdleState, WalkState 등 (상태 클래스들)

// Idle 상태
public class IdleState : IState
{
    public void Enter()
    {
        Debug.Log("Idle: Enter");
    }

    public void Execute()
    {
        Debug.Log("Idle: Execute");
    }

    public void Exit()
    {
        Debug.Log("Idle: Exit");
    }
}

// Walk 상태
public class WalkState : IState
{
    public void Enter()
    {
        Debug.Log("Walk: Enter");
    }

    public void Execute()
    {
        Debug.Log("Walk: Execute");
    }

    public void Exit()
    {
        Debug.Log("Walk: Exit");
    }
}

 

역할

실제 상태별 동작을 정의하는 클래스

Enter()에선 애니메이션 시작이나 초기화,
Execute()에선 반복 동작(예: 걷기),
Exit()에선 상태 전환 전 정리 작업 등을 수행

 

StateMachine

public class StateMachine
{
    private IState currentState;

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

    public void Update() // 생명주기 함수 Update 아님 주의(Execute로 이름 대체 가능)
    {
        currentState.Execute();
    }
}

역할

현재 상태를 기억하고,

새로운 상태로 전이(change)하며,

현재 상태의 로직을 실행(update)하는 중앙 관리자입니다.

즉, 상태 전환과 실행을 통제하는 컨트롤 타워 역할

 

왜 필요할까?

상태 객체만으로는 전환(변경)을 스스로 못함 → 누군가가 상태를 [관리]해줘야 함

StateMachine이 없으면, 상태끼리 서로 직접 호출해야 해서 코드가 복잡하고 꼬이게 됨

상태 전환을 일관되고 안전하게 처리할 수 있음

 

Player (FSM을 사용하는 MonoBehaviour)

public class Character : MonoBehaviour
{
    private StateMachine stateMachine;

    private void Start()
    {
        stateMachine = new StateMachine();
        stateMachine.ChangeState(new IdleState());
    }

    private void Update()
    {
        stateMachine.Update();
        if (Input.GetKeyDown(KeyCode.W))
        {
            stateMachine.ChangeState(new WalkState());
        }
        else if (Input.GetKeyDown(KeyCode.I))
        {
            stateMachine.ChangeState(new IdleState());
        }
    }
}

 


HFSM이란?

HFSM (Hierarchical Finite State Machine) 은 기존 FSM보다 한 단계 더 진화된 형태

FSM (Finite State Machine)  HFSM (Hierarchical FSM)
모든 상태가 동등한 계층 상태가 상위/하위 계층 구조
상태 전환만 있음 하위 상태, 공통 동작, 상속 구조 가능
예: Idle, Walk, Jump 예: Ground → Idle, Walk (하위 상태)

 

캐릭터 상태 예시

[GroundState]           ← 상위 상태 (공통 로직: 중력, 점프 감지 등)
   ├── [IdleState]         ← 멈춰 있을 때
   └── [WalkState]         ← 이동 중일 때

[AirState]              ← 공중 상태 (점프 중, 낙하 중)
   ├── [JumpState]
   └── [FallState]

IState (상태 인터페이스)

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

BaseState (공통된 기능을 묶음)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerBaseState : IState // 공통된 기능을 묶는 BaseState
{
    protected PlayerStateMachine stateMachine;
    protected readonly PlayerGroundData groundData;

    protected PlayerBaseState(PlayerStateMachine playerStateMachine)
    {
        stateMachine = playerStateMachine;
    }
    public virtual void Enter()
    {
        ... 생략
    }

    public virtual void Exit()
    {
        .. 생략
    }

    public virtual void HandleInput()
    {
        .. 생략
    }

    public virtual void Execute()
    {
        .. 생략
    }

    public virtual void PhysicsExecute()
    {
        
    }
 
    
    // 애니메이션 전환에 필요한 함수들
    protected virtual void StartAnimation(int animationHash)
    {
        stateMachine.Player.Animator.SetBool(animationHash, true);
    }

    protected virtual void StopAnimation(int animationHash)
    {
        stateMachine.Player.Animator.SetBool(animationHash, false);
    }


.... 중략

    
}

 

 

PlayerBaseState에서 생성자를 만들어 stateMachine을 저장하는 이유

모든 상태는 stateMachine에 접근해야함

상태 전환, 이동속도 계산, 입력 정보 등은 대부분 PlayerStateMachine에 담겨 있다.

때문에 상태를 만들 때 생성자로 stateMachine을 전달받아서 내부에 저장해 놓는다.

 

장점

모든 자식 상태 클래스들이 stateMachine을 상속받아 자동으로 사용할 수 있음
→ 예: stateMachine.Player.Animator 등으로 접근 가능

상태 간 전환 시, stateMachine.ChangeState(...)도 자식 클래스에서 바로 호출 가능

상태마다 별도 저장 안 해도 되므로 중복 제거 & 유지보수 용이

 

요약

PlayerBaseState는 상태들의 부모이고, 상태들이 stateMachine에 접근해야 하므로

생성자에서 한번에 받아 저장하는 구조

 

IdleState (GroundState 상속)

public class PlayerIdleState : PlayerGroundState
{
    public PlayerIdleState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        stateMachine.MovementSpeedModifier = 0f;
        base.Enter();
        StartAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
    }

    public override void Execute()
    {
        base.Execute();
    }
}

PlayerStateMachine

public class PlayerStateMachine : StateMachine
{
    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 MainCameraTransform { get; set; }
    
    public PlayerIdleState IdleState { get; private set; }

    public PlayerStateMachine(Player player)
    {
        this.Player = player;
        IdleState = new PlayerIdleState(this);
        ...중략
    }
}

 

PlayerStateMachine은 플레이어 전용 FSM으로

입력값, 속도, 점프력 등 캐릭터 제어에 필요한 데이터를 저장

IdleState 정보를 가지고 있음으로서 StateMachine에 상태(ChageState)에 접근할 수 있음

Player

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour // 입력 뿐만 아니라 여러가지 컴포넌트 관리해주는 클래스
{
    ...중략

    private PlayerStateMachine stateMachine;
    void Awake()
    {
       ...중략
        stateMachine = new PlayerStateMachine(this);
        stateMachine.ChangeState(stateMachine.IdleState);
    }

... 중략

    private void Update()
    {
        stateMachine.HandleInput();
        stateMachine.Execute();
    }

    private void FixedUpdate()
    {
        stateMachine.PhysicsExecute();
    }
}

 

stateMachine = new PlayerStateMachine(this);에서 this를 넘기는 이유와 장점

의미

this는 현재 Player 인스턴스 자신을 의미
즉, PlayerStateMachine을 생성하면서 현재 이 Player 객체에 대한 참조를 넘겨주는 것

 

왜 넘기나?

PlayerStateMachine 내부에서 플레이어 관련 데이터나 컴포넌트에 접근할 필요가 있기 때문

 

넘기면 좋은 이유

Player가 가진 Animator, CharacterController, Input, SOData 등에 접근할 수 있음

각 상태(State)가 stateMachine.Player를 통해 필요한 컴포넌트 사용 가능
예: stateMachine.Player.Animator.SetBool(...)

상태(State)에서 직접 Player를 참조하지 않고,

PlayerStateMachine → Player 경로를 통해 접근하도록 하여 의존성을 통제할 수 있음

 

📌 요약

this를 넘기면 PlayerStateMachine이 Player 객체에 접근할 수 있어서

상태 전환이나 컴포넌트 제어를 유기적으로 할 수 있다.