[Unity] 상태 패턴(FSM, HFSM)
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 객체에 접근할 수 있어서
상태 전환이나 컴포넌트 제어를 유기적으로 할 수 있다.