1. 유한 상태 머신(FSM)이란?
유한 상태 머신(FSM)은 상태의 유한 집합과 그 상태들 간의 전환을 정의하는 계산 모델입니다. 각 상태는 시스템이나 프로세스의 특정 조건을 나타내며, 이벤트나 조건의 발생에 따라 다른 상태로 전환할 수 있습니다.
상태(State)
상태는 시스템이 어떤 특정 시점에서 존재할 수 있는 모든 조건 또는 상황 중 하나를 의미합니다. 각 상태는 시스템의 행동, 즉 시스템이 수행하는 작업을 결정합니다. 예를 들어, 트래픽 신호등에서는 '빨간색', '노란색', '초록색'이라는 세 가지 상태가 있을 수 있으며, 각 상태는 신호등의 빛의 색을 결정합니다.
상태는 시스템의 특정 속성이나 상황을 나타내며, 시스템이 그 상태에 있을 때만 일정한 행동을 하거나 특정 이벤트를 기다립니다. 따라서, 상태는 동작의 문맥을 제공하며, 시스템이 어떻게 반응해야 할지를 정의하는 데 중요한 역할을 합니다.
상태 전이(State Transition)
상태 전이는 한 상태에서 다른 상태로의 이동을 의미합니다. 이 전이는 특정 조건이 충족되거나 이벤트가 발생했을 때 일어납니다. 상태 전이는 일반적으로 조건이나 이벤트에 의해 트리거 되며, 이를 '전이 조건'이라고 합니다.
상태 전이는 다음과 같은 형태로 표현될 수 있습니다.
- 출발 상태: 전이가 시작되는 상태
- 입력/이벤트: 상태 전이를 유발하는 조건 또는 이벤트
- 도착 상태: 전이 후에 시스템이 도달하는 상태
2. 구현 방법
구현 방법를 설명하기 위해 예시를 하나 제공하겠습니다.
상태 정의
- Idle (대기) - 적이 보이지 않을 때 기본 상태입니다.
- Patrol (순찰) - 특정 영역을 순찰하는 상태입니다.
- Chase (추격) - 적이 보이면 적을 추격하는 상태입니다.
- Attack (공격) - 적을 공격할 수 있는 상태입니다.
상태 전이 조건
- Idle 상태에서 Chase 상태로의 전이 조건: 적 발견
- Patrol 상태에서 Chase 상태로의 전이 조건: 적 발견
- Patrol 상태에서 Patrol 상태로의 전이 조건: 적 미발견, 순찰 계속
- Chase 상태에서 Attack 상태로의 전이 조건: 공격 가능
- Chase 상태에서 Idle 상태로의 전이 조건: 적 미발견
- Attack 상태에서 Idle 상태로의 전이 조건: 적 미발견
- Attack 상태에서 Attack 상태로의 전이 조건: 적 계속 공격
1) 조건문을 활용한 FSM 구현
유한 상태 머신(Finite State Machine, FSM)을 구현하는 가장 기본적인 방법 중 하나는 조건문(주로 switch-case 문이나 if-else 문)을 사용하는 것입니다. 구현이 단순하고 직관적이기 때문에, 이를 이용해 간단한 AI를 구현할 수 있습니다.
1. 상태 정의
FSM을 구현하기 전에, 시스템이 가질 수 있는 모든 가능한 상태를 정의해야 합니다. 일반적으로 이러한 상태들은 열거형(enum) 데이터 타입을 사용하여 표현됩니다.
public enum State
{
Idle,
Patrol,
Chase,
Attack
}
2. 초기 상태 설정
FSM의 시작 상태를 설정합니다. 이 상태는 시스템이 시작할 때의 상태로, 초기화 과정에서 정의됩니다. 초기화된 상태를 기준으로 현재 행동을 실행하고, 이후 로직에서 정의된 다른 상태로의 변경이 가능해집니다.
State currentState = State.Idle;
3. 조건문을 사용하여 상태 전이 로직 구현
각 상태에서 가능한 모든 이벤트(입력)를 고려하여, 해당 이벤트가 발생했을 때 시스템이 어떻게 반응해야 하는지를 정의합니다. 이 때 switch-case 문이나 if-else 문을 사용하여 각 상태에 따른 동작을 구현합니다.
switch (currentState) {
case State.Idle:
if (IsEnemyVisible()) {
currentState = State.Chase;
}
break;
case State.Patrol:
/* ... */
}
}
4. 상태 전이 조건 구현
상태를 변경하기 위해서는 상태 전이을 결정하는 데 필요한 조건을 구현해야 합니다. 예를 들어, 랜덤으로 탐색 중 범위 내 적이 확인되면, 추적 상태로 변경하는 조건을 감지하는 로직을 구현할 수 있습니다.
private bool IsEnemyVisible()
{
// 적 탐지 로직
return true; // 적을 발견했다고 가정
}
예시
public enum State {
Idle,
Patrol,
Chase,
Attack
}
public class EnemyAI {
private State currentState = State.Idle;
public void Update() {
switch (currentState) {
case State.Idle:
if (IsEnemyVisible()) {
currentState = State.Chase;
}
break;
case State.Patrol:
if (IsEnemyVisible()) {
currentState = State.Chase;
}
else {
PatrolArea();
}
break;
case State.Chase:
if (CanAttackEnemy()) {
currentState = State.Attack;
}
else if (!IsEnemyVisible()) {
currentState = State.Idle;
}
break;
case State.Attack:
if (!IsEnemyVisible()) {
currentState = State.Idle;
}
AttackEnemy();
break;
}
}
private bool IsEnemyVisible() {
// 적 탐지 로직
return true; // 적을 발견했다고 가정
}
private void PatrolArea() {
// 순찰 로직
}
private bool CanAttackEnemy() {
// 공격 가능 여부 판단 로직
return true; // 공격 가능하다고 가정
}
private void AttackEnemy() {
// 공격 실행 로직
}
}
조건문을 활용한 FSM 구현은 작은 규모의 프로젝트나 단순한 시나리오에서 효율적으로 동작하며, 복잡한 도구나 라이브러리 없이도 구현할 수 있는 장점이 있습니다. 또한, 빠른 개발과 테스트가 가능하며, 코드 수정 및 유지보수가 용이합니다.
그러나 단순 조건문 기반 FSM에서는 상태의 수가 증가함에 따라 코드의 복잡성이 기하급수적으로 증가하고 유지 관리가 어려워집니다. 모든 상태 전이를 하나의 함수나 메소드 내에서 관리해야 하므로 코드의 가독성이 저하되며, 상태 추가나 로직 변경 시 큰 수정이 필요할 수 있습니다.
2) 디자인 패턴을 적용한 FSM
디자인 패턴을 적용한 FSM은 코드의 구조와 유지보수성을 향상시키기 위해 고안된 방법입니다. 여기에 적용된 디자인 패턴인 상태 패턴(State Pattern)은 각 상태를 별도의 클래스로 캡슐화하여 각 상태의 행동을 클래스 내에서 독립적으로 관리할 수 있게 합니다. 이는 상태 추가나 변경을 용이하게 하며, 시스템의 확장성을 높여줍니다.
상태 패턴은 상태를 인터페이스나 추상 클래스로 정의하고, 각 상태를 구현하는 별도의 클래스를 만듭니다. 이 클래스들은 상태 인터페이스를 구현하며, 상태 전이는 각 상태 객체 내부에서 다음 상태 객체를 생성하여 반환함으로써 이루어집니다.
1. 상태 정의
상태 머신에서 각 상태는 독립적인 행동과 책임을 가진 객체로 표현됩니다. 이 단계에서는 시스템이 가질 수 있는 모든 가능한 상태를 정의합니다. 각 상태는 일반적으로 하나의 클래스로 구현되며, 모든 상태 클래스는 공통 인터페이스 또는 추상 클래스를 상속받습니다. 이 공통 인터페이스는 상태가 수행해야 할 기본 행동(예: Enter, Update, Exit 메서드)을 정의합니다.
public interface IState {
void Enter(Character character);
void Update(Character character);
void Exit(Character character);
}
public class IdleState : IState {
public void Enter(Character character) { /* 초기화 로직 */ }
public void Update(Character character) { /* 대기 상태에서 필요한 행동 구현 */ }
public void Exit(Character character) { /* 상태 종료 시 처리 */ }
}
public class WalkingState : IState {
public void Enter(Character character) { /* 초기화 로직 */ }
public void Update(Character character) { /* 걷기 상태에서 필요한 행동 구현 */ }
public void Exit(Character character) { /* 상태 종료 시 처리 */ }
}
2. 초기 상태 설정
상태 머신을 초기화할 때, 시작할 상태를 설정합니다. 이 초기 상태는 시스템이 처음 활성화될 때의 상태를 나타냅니다. 상태 머신 객체 내에서 현재 상태를 추적하는 변수를 사용하여 이를 관리합니다.
public class CharacterStateMachine {
private IState currentState;
public CharacterStateMachine() {
currentState = new IdleState(); // 초기 상태 설정
}
}
3. 상태 전이 로직 구현
상태 머신에서는 현재 상태에 따라 적절한 상태 객체의 메서드(예: Update, Enter, Exit)를 호출해야 합니다. 상태 전이 로직은 시스템의 상태를 다른 상태로 전환할 수 있도록 구현해야 하며, 이는 주로 현재 상태 객체에서 처리합니다.
public void Update() {
currentState.Update(this); // 현재 상태의 업데이트 로직 호출
}
public void ChangeState(IState newState) {
currentState.Exit(this); // 현재 상태 종료
currentState = newState; // 새 상태로 전환
currentState.Enter(this); // 새 상태 초기화
}
4. 상태 전이 조건 구현
각 상태에서 다른 상태로의 전이는 특정 조건이 만족될 때 발생합니다. 이 조건들은 각 상태의 Update 메서드 내에서 검사되며, 조건이 충족되면 상태 전이 로직이 호출됩니다. 조건은 외부 이벤트, 시간 제한, 게임 로직의 상태 등 다양한 요소에 기반할 수 있습니다.
public class WalkingState : IState {
public void Update(Character character) {
if (character.NeedsToStop()) {
character.ChangeState(new IdleState()); // 조건에 따라 상태 전이
}
}
}
예시
public interface IState {
void Enter(Character character);
void Update(Character character);
void Exit(Character character);
}
public class IdleState : IState {
public void Enter(Character character) { /* 초기화 로직 */ }
public void Update(Character character) {
if (character.SeesEnemy()) {
character.ChangeState(new ChaseState()); // 적 발견 시 Chase 상태로 전이
}
}
public void Exit(Character character) { /* 상태 종료 시 처리 */ }
}
public class PatrolState : IState {
public void Enter(Character character) { /* 초기화 로직 */ }
public void Update(Character character) {
if (character.SeesEnemy()) {
character.ChangeState(new ChaseState()); // 적 발견 시 Chase 상태로 전이
}
else {
character.ContinuePatrol(); // 적 미발견 시 순찰 계속
}
}
public void Exit(Character character) { /* 상태 종료 시 처리 */ }
}
public class ChaseState : IState {
public void Enter(Character character) { /* 초기화 로직 */ }
public void Update(Character character) {
if (character.CanAttack()) {
character.ChangeState(new AttackState()); // 공격 가능 시 Attack 상태로 전이
}
else if (!character.SeesEnemy()) {
character.ChangeState(new IdleState()); // 적을 잃으면 Idle 상태로 전이
}
}
public void Exit(Character character) { /* 상태 종료 시 처리 */ }
}
public class AttackState : IState {
public void Enter(Character character) { /* 초기화 로직 */ }
public void Update(Character character) {
if (!character.SeesEnemy()) {
character.ChangeState(new IdleState()); // 적을 잃으면 Idle 상태로 전이
}
else {
character.ContinueAttack(); // 적을 계속 공격
}
}
public void Exit(Character character) { /* 상태 종료 시 처리 */ }
}
public class Character {
private IState currentState;
public Character() {
currentState = new IdleState(); // 초기 상태 설정
}
public void Update() {
currentState.Update(this); // 현재 상태의 업데이트 로직 호출
}
public void ChangeState(IState newState) {
currentState.Exit(this); // 현재 상태 종료
currentState = newState; // 새 상태로 전환
currentState.Enter(this); // 새 상태 초기화
}
// 임의의 메서드들, 상태 전이 조건에 활용
public bool SeesEnemy() { /* 적을 발견하는 로직 */ }
public bool CanAttack() { /* 공격 가능한지 확인하는 로직 */ }
public void ContinuePatrol() { /* 순찰을 계속하는 로직 */ }
public void ContinueAttack() { /* 공격을 계속하는 로직 */ }
}
상태 패턴은 각 상태를 개별적으로 관리할 수 있어 유연성과 확장성이 뛰어나며, 코드의 가독성과 유지보수성이 향상됩니다.
그러나 초기 설계와 구현이 복잡할 수 있으며, 작은 프로젝트에서는 과도한 설계로 인식될 수 있습니다.
3. 결론
FSM은 복잡한 시스템의 상태를 관리하고 제어하는 강력한 도구입니다. 간단한 조건문 기반 FSM은 작은 규모의 시스템에 적합하나, 복잡한 시스템에서는 상태 패턴과 같은 디자인 패턴을 적용하는 것이 더 효과적입니다. 자신이 어느 수준의 AI를 개발하는지, 얼마나 복잡한 로직과 상태 전이 조건을 보유하게 될 지 판단하고 그에 맞춰 FSM을 설계하고 구현하는 것이 중요합니다.
'유니티' 카테고리의 다른 글
[Unity] 행동 트리(Behaviour Tree) (0) | 2024.05.04 |
---|