이 글은 해당 유튜브를 보고 진행한 것에 대해 공부한 것을 정리한 글입니다.
상태 (State)
게임을 개발하다 보면 오브젝트의 상태에 따라 동작을 달리해야 하는 상황을 자주 마주치게 됩니다.
예를 들어, 게임 캐릭터가 걷는 상태일 때는 이동 애니메이션이 재생되고, 공격 상태일 때는 공격 애니메이션이 실행되어야 합니다.
이런 경우 복잡한 switch 문이나 if문 대신 상태 패턴(State Pattern)을 활용하면 더 구조적인 설계를 할 수 있습니다.
상태 패턴 (State Pattern)
상태 패턴은 객체가 가지고 있는 내부 상태에 따라 행동이 바뀌도록 설계하는 디자인 패턴입니다.
객체는 내부 상태가 바뀌면 마치 클래스가 바뀐 것처럼 다른 방식으로 동작합니다.
상태 패턴은 각 상태를 별도의 클래스로 분리하고, 상태 전환을 클래스 간의 참조 변경을 통해 수행합니다.
따라서 상태별 동작을 독립적으로 정의할 수 있고, 기존 코드에 영향을 주지 않고 새로운 상태를 추가할 수 있습니다.
상태 패턴인 해결하는 문제 (by GOF)
원조 GOF에 따르면 상태 패턴은 다음 두 가지 문제를 해결합니다.
내부 상태 변경에 따른 동작 변경
- 객체는 내부 상태가 바뀜에 따라 동작 방식도 달라져야 할 수 있습니다. 상태 패턴은 이 문제를 해결하기 위해 동작을 상태에 위임합니다.
상태별 동작의 독립성
- 상태마다 동작을 별도의 클래스로 분리하면 확장 시에도 다른 상태에 영향을 주지 않기 때문에, 독립성이 보장되며 유지보수와 확장이 용이해집니다.
상태 패턴 사용 예시
캐릭터 애니메이션 상태 관리
- 걷기, 뛰기, 점프 등의 상태를 클래스로 분리하여 관리하면, 각 상태에 따른 애니메이션 재생 로직을 깔끔하게 구성할 수 있습니다.
네트워크 연결 상태
- 연결 중, 연결됨, 연결 실패 등 네트워크 상태에 따라 다른 로직이 실행될 수 있습니다.
툴 상태 (예 : 3D 툴의 선택, 이동, 회전 상태 등)
- 툴의 모드에 따라 클릭 이벤트나 마우스 입력에 반응하는 방식이 달라질 수 있습니다.
단순 AI
- 상태 패턴 기반으로 단순한 AI를 만들 수 있습니다.

장점
유연한 상태 전환
- 상태별 로직이 캡슐화되어 있어 상태 전환 로직을 각 클래스로 분리하여, 더 쉽게 독립적으로 관리하고 확장할 수 있습니다.
높은 가독성과 유지보수성
- if 문이나 switch 문 없이 상태 전환 로직이 각 상태 클래스에 분리되어 상태 클래스 자체에 동작이 정의되므로 코드 가독성이 좋아집니다.
- 상태를 추가 및 변경 시, 다른 상태 클래스의 영향 없이 독릭적으로 처리가 가능합니다.
- 각 상태는 상대적으로 크기가 작으며, 다른 상태로 전화하기 위한 조건만 추적합니다.
SOLID 원칙 준수
- 오브젝트에 대한 내부 로직을 설정할 때 SOLID 원칙을 준수하는 데 도움이 됩니다.
- 특히 OCP를 잘 따릅니다. 상태를 추가해도 기존 코드를 건드릴 필요가 없습니다.
단점
클래스 수 증가
- 상태가 많아질수록 그만큼의 클래스를 추가로 생성해야 하므로, 구조가 커질 수 있습니다.
복잡도 증가
- 복잡한 상태 전환 로직이 필요한 시스템에서는 상태 관리 클래스 간의 참조 관계나 의존성이 늘어나면서 구조가 다소 복잡해질 수 있습니다.
FSM, Finite State Machine (유한 상태 머신)
FSM(Finite State Machine)은 상태 관리의 대표적인 방식으로, 유한 상태 머신이라고 불립니다.
FSM은 어떤 시스템이 한 번에 하나의 상태(state)만을 가질 수 있으며, 이벤트나 조건에 따라 상태가 전이(transition)되도록 만든 구조입니다.
즉, 시스템이 가질 수 있는 모든 상태와, 한 상태에서 다른 상태로 전환하는 규칙을 정의하는 모델입니다.
FSM의 구성 요소
FSM은 4가지 요소로 구성됩니다.
상태 (State)
- 시스템이 가질 수 있는 각각의 상태
전환 (Transition)
- 한 상태에서 다른 상태로의 전환 규칙
이벤트 (Event)
- 상태 전환을 일으키는 외부 입력이나 조건
동작 (Action)
- 상태 전환 시, 또는 특정 상태에 있을 때 수행되는 행동
FSM의 특징
- FSM은 여러 가지 상태로 구성되며, 어떤 시점에서도 현재 상태 하나만 액티브 상태입니다.
- 각 상태는 런타임 시 조건에 따라 다른 한 가지 상태로의 전환으로 트리거할 수 있습니다.
- 전환이 발생하면 출력 상태는 새로운 액티브 상태로 전환됩니다.
enum과 switch 문을 사용하는 단순한 접근 방식을 통한 기본 FSM

public class UnrefactoredPlayerController : MonoBehaviour
{
private PlayerControllerState state;
private void Update()
{
GetInput();
switch (state)
{
case PlayerControllerState.Idle:
Idle();
break;
case PlayerControllerState.Walk:
Walk();
break;
case PlayerControllerState.Jump:
Jump();
break;
}
}
private void GetInput()
{
// process walk and jump controls
}
private void Walk()
{
// walk logic
}
private void Idle()
{
// idle logic
}
private void Jump()
{
// jump logic
}
다만, 위의 코드와 같이 작성시, 작동은 하더라도 상태와 복잡도를 추가할 때 해당 스크립트를 매번 재확인해야 하므로 유지보수가 떨어집니다.
이를 해결하기 위해 스테이트 구성을 위한 인터페이스를 만들고, 그 인터페이스를 상속받는 각각의 상태를 클래스로 만듭니다.
이후, 스테이트 머신 클래스를 만들어 인터페이스와 상태 클래스를 통해 스테이트 머신을 구성합니다.
인터페이스를 활용한 FSM

위의 이미지에서, 상태를 입력하고 조건에 의해 컨트롤 플로가 종료될 때까지 각 프레임을 계속 루프 처리합니다.
이 패턴을 구현하기 위해 IState 인터페이스를 만듭니다.
public interface IState
{
public void Enter()
{
// code that runs when we first enter the state
}
public void Update()
{
// per-frame logic. +condition to transition to a new state
}
public void Exit()
{
// code that runs when we exit the state
}
}
그리고 IState 인터페이스를 상속받는 각각의 상태 클래스를 만듭니다.
이때 구현을 구체화합니다.
public class IdleState : IState
{
private PlayerController player;
// pass in any parameters you need in the constructors
public IdleState(PlayerController player) { this.player = player; }
public void Enter()
{
// Code executed when entering the status for the first time
}
// per-frame logic, include condition to transition to a new state
public void Update()
{
if (!player.IsGrounded)
{ // if we're no longer grounded, transition to jumping
player.PlayerStateMachine. TransitionTo(player.PlayerStateMachine.jumpState);
}
// if we move above a minimum threshold, transition to walking
if (Mathf.Abs(player.CharController.velocity.x) > 0.1f ||
Mathf.Abs(player.CharController.velocity.z) > 0.1f)
{
player.PlayerStateMachine. TransitionTo(
player.PlayerStateMachine.walkState);
}
// ...
}
public void Exit()
{
// Code executed when out of state
}
}
StateMachine 클래스는 컨트롤 플로가 상태에 진입하고 상태에서 벗어나는 방법을 관리합니다.
스테이트머신은 MonoBeahviour에서 상속받지 않으므로 생성자를 사용하여 각 인스턴스를 설정합니다.
[Serializable]
public class StateMachine
{
public IState CurrentState { get; private set; }
public WalkState walkState;
public JumpState jumpState;
public IdleState idleState;
public StateMachine(PlayerController player)
{
this.walkState = new WalkState(player);
this.jumpState = new JumpState(player);
this.idleState = new IdleState(player);
}
public void Initialize(IState startingState)
{
CurrentState = startingState;
startingState.Enter();
}
public void TransitionTo(IState nextState)
{
CurrentState.Exit();
CurrentState = nextState;
nextState.Enter();
}
public void Update()
{
if (CurrentState != null)
{
CurrentState.Update();
}
}
}
StateMachineBehaviour
StateMachineBehviour는 Unity의 기본 클래스로, Animator 상태에만 붙는 스크립트입니다.
MonoBehaviour 기반이 아닌 ScriptableObject 기반으로, 씬의 오브젝트가 아닌 Animator 내부 상태에 종속됩니다.
따라서 GameObject, GetComponent, transform 등 직접적인 참조가 불가능합니다.
namespace UnityEngine
{
///StateMachineBehaviour is a component that can be added to a state machine state.
///It's the base class every script on a state derives from
[RequiredByNativeCode]
public abstract class StateMachineBehaviour : ScriptableObject
{
public virtual void OnStateEnter(
Animator animator, AnimatorStatelnfo statelnfo, int layerIndex) { }
public virtual void OnStateUpdate(
Animator animator, AnimatorStatelnfo statelnfo, int layerIndex) { }
public virtual void OnStateExit(
Animator animator, AnimatorStatelnfo statelnfo, int layerlndex) { }
public virtual void OnStateMove(
Animator animator, AnimatorStatelnfo statelnfo, int layerIndex) { }
public virtual void OnStatelK(
Animator animator, AnimatorStatelnfo statelnfo, int layerIndex) { }
}
// ...
}
HFSM, Hierarchical Finite State Machine (계층적 유한 상태 머신)
HFSM은 기존의 FSM에 계층 구조(Hierarchy) 개념을 도입한 설계 방식입니다.
즉, 상태 안에 또 다른 상태 머신을 포함시킬 수 있는 구조로, 복잡한 상태를 보다 모듈화하고 체계적으로 구성할 수 있도록 도와줍니다.
기본 FSM은 단순한 로직에서는 충분하지만, 복잡한 캐릭터 동작이나 AI, 혹은 다양항 상황 전환이 필요한 시스템에서는 상태 수가 폭발적으로 증가하고 전이 조건 관리가 매우 어려워집니다.
이러한 문제를 해결하기 위해 상태를 그룹화하고, 상위 상태와 하위 상태 간의 흐름을 정의할라 수 있는 HFSM이 필요합니다.
주로 상속 또는 컴포지션 방식으로 구현 가능하며, Unity에서는 ScriptableObject 또는 Class 기반 구현이 많습니다.
예시
| FSM 구조 | HFSM 구조 |
| • Idle • Walk • Run • Jump • Attack • Die |
• Grounded • Idle • Walk • Run • InAir • Jump • Fall • Combat • Attack • Defend • Die |
HFSM의 구성 요소
상위 상태 (Super State)
- 여러 하위 상태를 포함하는 상태
하위 상태 (Sub State)
- 상위 상태 내에서의 구체적인 행동 상태
전환 (Transition)
- 한 상태에서 다른 상태로의 전환 규칙
이벤트 (Event)
- 상태 전환을 일으키는 외부 입력이나 조건
동작 (Action)
- 상태 전환 시, 또는 특정 상태에 있을 때 수행되는 행동
BT, Behaviour Tree (행동 트리)
BT는 행동 결정 로직을 트리 형태로 표현한 구조입니다.
주로 게임 AI에서 사용되며, 복잡한 의사 결정 구조를 간결하고 모듈화된 방식으로 설계할 수 있게 해줍니다.
FSM, HFSM보다 구조가 더 유연하고 유지보수에 적합한 방식입니다.
FSM 구조나 HFSM 구조는 각 상태마다 지닌 로직을 통해 다음 상태로 전이했다고 하면, BT 구조는 상위 노드에서 매 틱마다 우선 순위를 평가하고 상태를 결정합니다.
BT 기본 구성 요소
루트 노드 (Root Node)
- 트리의 최상위 노드입니다.
- 실행의 시작점으로, 상태를 정해줍니다.
컴포지트 노드 (Composite Node)
- 자식 노드를 여러 개 가집니다.
- 자식 노드들의 실행 결과에 따라 자신이 성공할지 실패할지를 결정하여 상태를 정하는데 우선순위를 평가합니다.
- 대표적으로 Selector, Sequence가 존재합니다.
데코레이터 노드 (Decorator Node)
- 하나의 자식 노드를 가집니다.
- 자식 노드의 결과를 수정하거나 조건을 붙이는 역할을 합니다.
리프 노드 (Leaf Node)
- Execution 노드로도 불리며, 실제 동작을 수행하는 노드로 구체적인 행동을 정의합니다.
- 리프 노드는 더 이상 자식을 가지지 않습니다.

몹 AI (3D Game Kit) : 다른 활용 예시
3D Game Kit Lite | 자습서 | Unity Asset Store
Use 3D Game Kit Lite from Unity Technologies to elevate your next project. Find this & more 자습서 and templates on the Unity Asset Store.
assetstore.unity.com
해당 에셋은 Unity에서 제공하는 3D Game Kit 입니다. 이 에셋에서 몬스터는 상태 패턴으로 구현되어있습니다.
아래의 코드들은 몬스터가 상태 패턴으로 동작하기 위한 코드들입니다.
State 구성
해당 클래스는 애니메이션 상태 머신의 동작을 황장하여 사용하는 구조입니다.
StateMachineBehaviour를 상속받고 있기 때문에, MonoBehaviour를 사용하지 못하여 Generic으로 확장해서 사용합니다.
Initialise 메서드
- FSM을 사용하는 Animator에 연결된 모든 SceneLinkedSMB<TMonoBehaviour>를 찾아서 초기화합니다.
- 이 메서드는 수동으로 호출되어야 하며, 일반적으로 오브젝트가 생성되었을 때 사용됩니다.
내부 클래스 : SealedSMB
- Unity 기본 클래스인 StateMachineBehaviour를 상속합니다.
- sealed override 메서드를 통해 파생 클래스가 상태별 동작을 제어 가능한 지점에서만 확장하도록 제한합니다.
상태 콜백 메서드
- OnStateEnter
- 상태가 진입될 때 실행됩니다.
- 하위 클래스에서는 OnSLStateEnter 메서드를 override하여 원하는 동작을 수행할 수 있도록 설계되어 있습니다.
- OnStateUpdate
- 상태가 업데이트되는 동안 매 프레임마다 호출됩니다.
- 애니메이터가 현재 전이(Transition) 중인 경우 OnSLTransitionToStateUpdate가 호출되어 적절한 처리를 수행합니다.
public class SceneLinkedSMB<TMonoBehaviour> : SealedSMB where TMonoBehaviour : MonoBehaviour
{
protected TMonoBehaviour m_MonoBehaviour;
// ...
public static void Initialise (Animator animator, TMonoBehaviour monoBehaviour)
{
SceneLinkedSMB<TMonoBehaviour>[] sceneLinkedSMBs =
animator.GetBehaviours<SceneLinkedSMB<TMonoBehaviour>>();
for (int i = 0; i < sceneLinkedSMBs.Length; i++)
{
sceneLinkedSMBs[i].Internallnitialise(animator, monoBehaviour);
}
}
protected void Internallnitialise (Animator animator, TMonoBehaviour monoBehaviour)
{
m_MonoBehaviour = monoBehaviour;
OnStart (animator);
}
public abstract class SealedSMB : StateMachineBehaviour
{
// ...
}
// ...
public sealed override void OnStateEnter(Animator animator, AnimatorStatelnfo statelnfo,
int layerIndex,AnimatorControllerPlayable controller)
{
m_FirstFrameHappened = false;
OnSLStateEnter(animator, statelnfo, layerIndex);
OnSLStateEnter (animator, statelnfo, layerIndex, controller);
}
public sealed override void OnStateUpdate(Animator animator, AnimatorStatelnfo statelnfo,
int layerIndex, AnimatorControllerPlayable controller)
{
if(!animator.gameObject.activeSelf) return;
if (animator.IsInTransition(layerIndex) &&
animator.GetNextAnimatorStateInfo(layerIndex).fullPathHash == statelnfo.fullPathHash)
{
OnSLTransitionToStateUpdate(animator, statelnfo, layerIndex);
OnSLTransitionToStateUpdate(animator, statelnfo, layerIndex, controller);
}
// ...
}
// ...
}
ChomperBehaviour
해당 클래스는 적 캐릭터 Chomper의 행동 로직을 제어하는 스크립트입니다.
애니메이션 상태 전환, 추적 시작, 공격 트리거 등을 다루고 있습니다.
이 클래스는 Chomper 캐릭터에 직접 붙어 있는 MonoBehaviour 스크립트로, 내부의 복잡한 상태 제어나 FSM 구현에 대한 세부사항을 알 필요 없이 사용할 수 있다는 장점이 있습니다.
예를 들어, ChomperSMBIdle이나 ChomperSMBAttack과 같은 애니메이션 상태(StateMachineBehaviour) 클래스에서는 이 ChomperBehaviour 클래스에 접근해 필요한 행동 메서드(StartPursuit( ), TriggerAttack( ) 등)를 호출하기만 하면 됩니다.
이처럼 상태 클래스와 행동 로직이 분리되어 설계되어 있기 때문에, 각 상태에서 어떤 행동을 할지 결정할 수 있고, 기존 코드를 건드리지 않아도 새로운 행동을 추가하거나 변경할 수 있어 유지보수가 매우 용이합니다.
IMessageReceiver
- 메시지를 수신하는 인터페이스로, FSM 상태 전환 시 특정 행동을 수행하도록 설계되어 있습니다.
StartPursuit( ) 메서드
- Chomper가 m_FollowerInstance가 null이 아닐 경우 타겟을 따라가는 상태로 전환됩니다.
- m_Controller.animator.SetBool(hashInPursuit, true); 를 통해 애니메이션 상태 전환을 유도합니다.
TriggerAttack( ) 메서드
- 공격을 트리거하는 메서드입니다.
- Animator의 "hashAttack" 트리거를 작동시켜 공격 애니메이션 상태로 전환합니다.
public class ChomperBehavior : MonoBehaviour, IMessageReceiver
{
public static readonly int hashlnPursuit = Animator.StringToHash("InPursuit");
public static readonly int hashAttack = Animator.StringToHash("Attack");
public EnemyController controller { get { return m_Controller; } }
// ...
public void StartPursuit()
{
if (m_FollowerInstance != null)
{
m_FollowerInstance.requireSlot = true;
RequestTargetPosition();
}
m_Controller.animator.SetBool(hashlnPursuit, true);
}
// ...
public void TriggerAttack()
{
m_Controller.animator.SetTrigger(hashAttack);
}
// ...
}
Chomper의 상태 클래스 : Idle, Attack, Pursuit
아래의 스크립트들은 Chomper의 상태 클래스입니다.
이런 경우에 heal 상태가 필요하다고 하면, 다른 곳은 수정이 필요 없고, heal만 추가하면 됩니다.
디자인 패턴은 구현때는 좀 힘들 수 있지만, 구현 후의 유지보수에 있어서 압도적으로 편합니다.
public class ChomperSMBIdle : SceneLinkedSMB<ChomperBehavior>
{
public float minimumldleGruntTime = 2.0f;
public float maximumldleGruntTime = 5.0f;
protected float remainingToNextGrunt = 0.Of;
public override void OnStateMachineEnter(Animator animator, int stateMachinePathHash)
{
if (minimumldleGruntTime > maximumldleGruntTime)
minimumldleGruntTime = maximumldleGruntTime;
remainingToNextGrunt = Random.Range(minimumldleGruntTime, maximumldleGruntTime);
}
public override void OnSLStateNoTransitionUpdate(
Animator animator, AnimatorStatelnfo statelnfo, int layerIndex)
{
base.OnSLStateNoTransitionUpdate(animator, statelnfo, layerIndex);
remainingToNextGrunt -= Time.deltaTime;
if (remainingToNextGrunt < 0)
{
remainingToNextGrunt = Random.Range(minimumldleGruntTime, maximumldleGruntTime);
m_MonoBehaviour.Grunt();
m_MonoBehaviour.FindTarget();
if (m_MonoBehaviour.target != null)
{
m_MonoBehaviour.StartPursuit();
}
}
// ...
}
public class ChomperSMBAttack : SceneLinkedSMB<ChomperBehavior>
{
protected Vector3 m_AttackPosition;
public override void OnSLStateEnter(Animator animator, AnimatorStatelnfo statelnfo)
{
base.OnSLStateEnter(animator, statelnfo, layerIndex);
m_MonoBehaviour.controller.SetFollowNavmeshAgent(false);
m_AttackPosition = m_MonoBehaviour.target.transform.position;
Vector3 toTarget = m_AttackPosition - m_MonoBehaviour.transform.position;
toTarget.y = 0;
m_MonoBehaviour.transform.forward = toTarget.normalized;
m_MonoBehaviour.controller.SetForward(m_MonoBehaviour.transform.forward);
if (m_MonoBehaviour.attackAudio != null)
m_MonoBehaviour.attackAudio.PlayRandomClip();
}
// ...
}
public class ChomperSMBPursuit : SceneLinkedSMB<ChomperBehavior>
{
public override void OnSLStateNoTransitionUpdate(Animator animator, AnimatorStatelnfo stateInfo)
{
base.OnSLStateNoTransitionUpdate(animator, statelnfo, layerIndex);
m_MonoBehaviour.FindTarget();
if (m_MonoBehaviour.controller.navmeshAgent.pathStatus == NavMeshPathStatus.PathComplte
|| m_MonoBehaviour.controller.navmeshAgent.pathStatus == NavMeshPathStatus.PathComplete)
{
m_MonoBehaviour.StopPursuit();
return;
}
if (m_MonoBehaviour.target == null || m_MonoBehaviour.target.respawning)
{
m_MonoBehaviour.StopPursuit();
}
else
{
m_MonoBehaviour.RequestTargetPosition();
Vector3 toTarget
= m_MonoBehaviour.target.transform.position - m_MonoBehaviour.transform.position;
if (toTarget.sqrMagnitude < m_MonoBehaviour.attackDistance * m_MonoBehaviour.attackDistance)
{
m_MonoBehaviour. TriggerAttack();
}
else if (m_MonoBehaviour.followerData.assignedSlot != -1)
{
// ...
}
}
// ...
}
'Unity' 카테고리의 다른 글
| [Unity] 디자인 패턴 : Command 패턴 (0) | 2025.06.13 |
|---|---|
| [Unity] 디자인 패턴 : Observer 패턴 (2) | 2025.06.13 |
| [Unity] 디자인 패턴 : SOLID 원칙 (1) | 2025.06.09 |
| [Unity] Unity 2D 게임 개발 정리 (4) (0) | 2025.06.04 |
| [Unity] Unity 2D 게임 개발 정리 (3) (0) | 2025.06.03 |