유한 상태 머신이란?
- 시스템이 가질 수 있는 모든 상태를 정의하고, 각 상태 간의 전환 조건을 명확히 정의하는 패턴
- '유한'하다는 것은 시스템이 가질 수 있는 상태의 개수가 제한적이라는 의미
핵심 구성 요소
1. 상태
- 게임 캐릭터의 idle, walk, run, jump 등 시스템이 가질 수 있는 특정 조건이나 상황
2. 전이
- Space키를 누르면 idle에서 jump로 바뀌는 등 한 상태에서 다른 상태로 변화하는 조건
3. 이벤트
- 키보드 입력, 타이머, 충돌 등 상태 전이를 촉발하는 트리거
4. 액션
- 애니메이션 재생, 효과음 재생 등 상태 진입/퇴장시 실행하는 동작
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
장점
1. 명확한 로직 구조
- 상태와 전이가 명확히 정의되어 있어 코드의 가독성이 높음
- 디버깅이 용이함
2. 유지보수 용이성
- 새로운 상태나 전이 추가가 쉬움
- 각 상태가 독립적으로 관리됨
3. 예측 가능성
- 시스템의 동작을 예측하기 쉬움
- 버그 발생 가능성이 낮음
단점
1. 상태 폭팔
- 상태가 많아질수록 관리가 복잡해질 수 있음
- 전이 조건이 복잡해질 수 있음
2. 유연성 제한
- 동시에 여러 상태를 가질 수 없음
- 복잡한 조건의 표현이 어려울 수 있음
질문
Q. FSM의 가장 큰 장점은?
A. 명확한 로직 구조와 유지보수 용이성 입니다.
Q. 상태 폭팔이란?
- 상태가 많아질수록 관리해야할 것들이 많아지고 복잡해 지는 것입니다.
Q. 실제 게임에서 FSM을 어떻게 활용할 수 있을까요?
A. 몬스터, 캐릭터 등에 사용 가능
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
몬스터 AI 예시
public enum MonsterState
{
Idle, // 대기
Patrol, // 순찰
Chase, // 추적
Attack, // 공격
Damaged, // 피격
Die // 사망
}
public class MonsterFSM : MonoBehaviour
{
private MonsterState currentState;
private float detectionRange = 5f; // 감지 범위
private float attackRange = 2f; // 공격 범위
void Update()
{
switch (currentState)
{
case MonsterState.Idle:
UpdateIdleState();
break;
case MonsterState.Patrol:
UpdatePatrolState();
break;
// ... 다른 상태들
}
}
// 대기 상태 업데이트
private void UpdateIdleState()
{
// 플레이어가 감지 범위 안에 들어오면 Chase 상태로 전환
if (IsPlayerInRange(detectionRange))
{
ChangeState(MonsterState.Chase);
}
}
}
캐릭터 상태 예시
public enum PlayerState
{
Idle,
Walk,
Run,
Jump,
Attack,
Dead
}
public class PlayerController : MonoBehaviour
{
private PlayerState currentState;
private Animator animator;
// 상태 변경 메서드
private void ChangeState(PlayerState newState)
{
// 이전 상태 종료 작업
OnExitState(currentState);
// 새로운 상태 설정
currentState = newState;
// 새로운 상태 시작 작업
OnEnterState(currentState);
}
}
Q. 몬스터 FSM에서 각 상태 간 전환이 일어나는 조건들은 어떤 것이 있을까요?
A. UpdateIdleState와 UpdatePatrolState가 있습니다.
Q. 캐릭터 상태 관리에서 OnExitState와 OnEnterState는 각각 어떤 역할을 할까요?
A. 상태로 들어갔을때 나갔을때 입니다.
Q. FSM을 구현할 때 주의해야 할 점은 무엇일까요?
A. 명확한 로직을 기준으로 구현을 해야합니다
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
구현 방법
1. 기본 인터페이스 구조
// 상태 인터페이스
public interface IState
{
void Enter(); // 상태 진입 시 호출
void Update(); // 상태 업데이트
void Exit(); // 상태 종료 시 호출
}
// 기본 상태 클래스
public abstract class StateBase : IState
{
protected FSMController controller; // FSM 컨트롤러 참조
public StateBase(FSMController controller)
{
this.controller = controller;
}
public virtual void Enter() { }
public virtual void Update() { }
public virtual void Exit() { }
}
2. 상태 구현 예시
// 대기 상태 구현
public class IdleState : StateBase
{
public IdleState(FSMController controller) : base(controller) { }
public override void Enter()
{
// 애니메이션 전환
controller.animator.SetTrigger("Idle");
}
public override void Update()
{
// 이동 입력 확인
if (controller.GetMovementInput() != Vector2.zero)
{
controller.ChangeState(new WalkState(controller));
}
// 공격 입력 확인
if (controller.GetAttackInput())
{
controller.ChangeState(new AttackState(controller));
}
}
public override void Exit()
{
// 대기 상태 종료 시 필요한 정리 작업
controller.animator.ResetTrigger("Idle");
}
}
3. FSM컨트롤러
public class FSMController : MonoBehaviour
{
private IState currentState;
public Animator animator { get; private set; }
void Start()
{
animator = GetComponent<Animator>();
// 초기 상태 설정
ChangeState(new IdleState(this));
}
void Update()
{
// 현재 상태 업데이트
currentState?.Update();
}
public void ChangeState(IState newState)
{
// 이전 상태 종료
currentState?.Exit();
// 새로운 상태로 전환
currentState = newState;
// 새로운 상태 시작
currentState?.Enter();
}
// 입력 처리 메서드들
public Vector2 GetMovementInput()
{
return new Vector2(Input.GetAxisRaw("Horizontal"),
Input.GetAxisRaw("Vertical"));
}
public bool GetAttackInput()
{
return Input.GetButtonDown("Fire1");
}
}
질문
Q. IState 인터페이스의 각 메서드(Enter, Update, Exit)는 언제 호출되나요?
A. Update는 매프레임, Enter,Exit는 ChangeState로 상태가 변경될때 입니다.
Q. FSMController의 역할은 무엇인가요?
A. FSMController 이름 그자체로 FSM의 상태를 변경하고 처리하는 컨트롤러 입니다.
Q. 새로운 상태를 추가하려면 어떤 작업들이 필요할까요?
A. 새로운 상태 클래스를 생성하고 StateBase를 상속받은 다음 구현합니다. 이후 변경이 필요할때 FSMController의 ChangeState를 이용해 변경해줍니다.
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
실전 예제 - 적 AI FSM 구현
// 적 상태 정의
public enum EnemyState
{
Idle,
Patrol,
Chase,
Attack,
Retreat,
Stunned,
Dead
}
// 적 FSM 구현
public class EnemyFSM : MonoBehaviour
{
private Dictionary<EnemyState, IState> states;
private IState currentState;
[Header("Stats")]
public float health = 100f;
public float moveSpeed = 5f;
public float detectRange = 10f;
public float attackRange = 2f;
[Header("Components")]
public Animator animator;
public NavMeshAgent agent;
private void Awake()
{
// 상태 객체들을 미리 생성하여 Dictionary에 저장
states = new Dictionary<EnemyState, IState>()
{
{ EnemyState.Idle, new EnemyIdleState(this) },
{ EnemyState.Patrol, new EnemyPatrolState(this) },
{ EnemyState.Chase, new EnemyChaseState(this) },
{ EnemyState.Attack, new EnemyAttackState(this) },
{ EnemyState.Retreat, new EnemyRetreatState(this) },
{ EnemyState.Stunned, new EnemyStunnedState(this) },
{ EnemyState.Dead, new EnemyDeadState(this) }
};
}
private void Start()
{
// 초기 상태 설정
ChangeState(EnemyState.Idle);
}
private void Update()
{
// 현재 상태 업데이트
currentState?.Update();
}
public void ChangeState(EnemyState newState)
{
// 현재 상태가 있다면 종료
currentState?.Exit();
// 새로운 상태로 전환
currentState = states[newState];
// 새로운 상태 시작
currentState.Enter();
Debug.Log($"Changed to state: {newState}");
}
// 플레이어와의 거리 계산
public float GetDistanceToPlayer()
{
Transform player = GameObject.FindGameObjectWithTag("Player").transform;
return Vector3.Distance(transform.position, player.position);
}
// 데미지 처리
public void TakeDamage(float damage)
{
health -= damage;
if (health <= 0)
{
ChangeState(EnemyState.Dead);
}
else if (currentState is not EnemyStunnedState)
{
ChangeState(EnemyState.Stunned);
}
}
}
최적화 포인트
- 상태 객체를 Dictionary에 캐싱하여 재사용
- GetDistanceToPlayer()는 매 프레임 호출될 수 있으므로 최적화 필요
- FindGameObjectWithTag는 비용이 크므로 캐싱 필요
질문
Q. Dictionary를 사용하여 상태를 관리하는 것의 장점은 무엇인가요?
A. 미리 캐싱하여 구현해두어 가비지 발생이 덜합니다.
Q. 이 구현에서 개선이 필요한 부분은 어디일까요?
A. GetDistanceToPlayer()의 FindGameObjectWithTag부분 최적화 필요
Q. FSM을 사용한 AI 구현의 장단점은 무엇일까요?
A. 여러 복잡할 수 있는 상황들을 구조에 맞춰서 가독성 있게 제작이 가능합니다.
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
성능 최적화 및 고급 패턴
1. 최적화된 구현
public class OptimizedEnemyFSM : MonoBehaviour
{
// 캐싱을 위한 필드
private Transform playerTransform;
private float sqrDetectRange; // 제곱된 감지 범위
private Vector3 lastKnownPlayerPos;
[SerializeField] private float detectRange = 10f;
private void Awake()
{
// 초기화 시 한 번만 플레이어 찾기
playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
// 제곱 거리 미리 계산 (최적화)
sqrDetectRange = detectRange * detectRange;
}
// 최적화된 거리 체크
public bool IsPlayerInRange()
{
// magnitude 대신 sqrMagnitude 사용 (제곱근 계산 제거)
return (playerTransform.position - transform.position).sqrMagnitude <= sqrDetectRange;
}
}
2 계층적 FSM (HFSM) 구현
// 상위 상태 (메타 상태) 정의
public abstract class MetaState : StateBase
{
protected IState currentSubState;
protected MetaState(FSMController controller) : base(controller) { }
public void UpdateSubState()
{
currentSubState?.Update();
}
protected void ChangeSubState(IState newState)
{
currentSubState?.Exit();
currentSubState = newState;
currentSubState?.Enter();
}
}
// 전투 상태 예시
public class CombatState : MetaState
{
public CombatState(FSMController controller) : base(controller)
{
// 초기 서브상태 설정
ChangeSubState(new AttackState(controller));
}
public override void Update()
{
// 전투 상태 로직
UpdateSubState();
// 전투 종료 조건 체크
if (!controller.IsEnemyNearby())
{
controller.ChangeState(new PatrolState(controller));
}
}
}
추가 최적화 팁
- Vector3.Distance 대신 sqrMagnitude 사용
- 상태 전환 시 가비지 생성 최소화
- Physics 체크는 FixedUpdate에서 처리
- 코루틴 대신 상태 패턴 활용으로 메모리 할당 최소화
sqrMagnitude vs magnitude
// 방법 1: magnitude 사용
float distance = (playerPos - myPos).magnitude;
// 실제 계산: sqrt(x² + y² + z²)
// 방법 2: sqrMagnitude 사용
float sqrDistance = (playerPos - myPos).sqrMagnitude;
// 실제 계산: x² + y² + z²
- magnitude는 제곱근(sqrt) 계산을 포함하는데, 이는 CPU에 부담이 큽니다. 반면 sqrMagnitude는 제곱근 계산을 하지 않습니다.
거리 비교 시 코드
// 기존 방식
if (Vector3.Distance(transform.position, target.position) < detectRange)
// 최적화된 방식
if ((transform.position - target.position).sqrMagnitude < detectRange * detectRange)
계층적 FSM (HFSM)의 특징
1. 일반 FSM
Idle → Patrol → Combat → Dead
- 모든 상태가 동일한 레벨
- 상태 전환이 복잡해질 수 있음
2. 계층적 FSM (HFSM)
일반 FSM:
Idle → Walk → Run → Attack → Dead
(모든 상태가 개별적으로 존재)
HFSM:
[Peaceful 상태]
├─ Idle
└─ Walk
[Combat 상태]
├─ Attack
├─ Block
└─ Dodge
[Dead 상태]
- 관련된 상태들을 그룹화
- 코드 재사용성 증가
- 상태 관리가 더 체계적
HFSM 실제 코드
// 상위 상태 (Combat)
public class CombatState : MetaState
{
public override void Enter()
{
// Combat 상태 진입 시 공통 작업
controller.DrawWeapon();
controller.PlayCombatMusic();
// 기본 하위상태 설정
ChangeSubState(new CombatIdleState(controller));
}
public override void Update()
{
// Combat 상태의 공통 업데이트
controller.UpdateStamina();
controller.CheckEnemyDistance();
// 하위상태 업데이트
UpdateSubState();
}
public override void Exit()
{
controller.SheathWeapon();
controller.StopCombatMusic();
}
}
// 하위 상태 (CombatIdle)
public class CombatIdleState : StateBase
{
public override void Enter()
{
controller.PlayCombatIdleAnimation();
}
public override void Update()
{
// 공격 입력 체크
if (controller.GetAttackInput())
{
controller.GetMetaState<CombatState>()
.ChangeSubState(new AttackState(controller));
}
}
}
HFSM의 장점
1. 코드 구조화
- 관련된 상태들을 논리적으로 그룹화
- 예: 평화/전투/사망 처럼 큰 범주 구분
2. 코드 재사용
- 상위 상태에서 공통 로직 처리
- 하위 상태에서 세부 동작만 구현
3. 유지보수 용이
- 상태 관리가 체계적
- 새로운 상태 추가가 쉬움
대표적인 예시
Movement(이동) 상위 상태
Idle (정지)
Walk (걷기)
Run (달리기)
Crouch (앉기)
Jump (점프)
Roll (구르기)
Combat(전투) 상위 상태
CombatIdle (전투 대기)
Attack (공격)
Block (방어)
Dodge (회피)
Skill (스킬 사용)
Reload (재장전)
Interaction(상호작용) 상위 상태
Talk (대화)
Shopping (상점)
Crafting (제작)
Reading (읽기)
Mining (채광)
Fishing (낚시)
Status(상태이상) 상위 상태
Stunned (기절)
Poisoned (중독)
Frozen (빙결)
Burned (화상)
Sleeping (수면)
Confused (혼란)
AI(인공지능) 상위 상태
Patrol (순찰)
Chase (추적)
Search (수색)
Escape (도망)
Guard (경비)
Alert (경계)
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
FSM 구현 시 핵심 권장사항
1. 설계 단계
- 상태 다이어그램을 먼저 그리고 시작
- 상태 전환 조건을 명확히 정의
- 계층 구조가 필요한지 먼저 판단
2. 구현 단계
- 상태 패턴을 활용한 깔끔한 구조화
- 성능을 고려한 최적화 (객체 재사용, 캐싱)
- 디버깅을 위한 로그 시스템 구현
3. 유지보수 단계
- 새로운 상태 추가 시 기존 상태들과의 관계 검토
- 성능 모니터링 도구 활용
- 정기적인 코드 리뷰
4. 주의사항
- 상태 폭발 방지를 위한 적절한 계층화
- 순환 참조 방지
- 불필요한 상태 전환 최소화
'유니티 > 패턴, 코드 및 이론 정리' 카테고리의 다른 글
[간단 정리] 가비지 컬랙션 (0) | 2025.01.22 |
---|---|
[간단 정리] 다국어 처리 (1) | 2025.01.22 |
[간단 정리] JSON (1) | 2025.01.22 |
[패턴] MVC, MVP, MVVM (1) | 2025.01.14 |