오토마타 이론
1.게임 제작을 하는데 왜 오토마타 이론을 배워야 하는가 ?
이 이론은 상태(state)와 이벤트(event)를 처리하는 시스템을 모델링하고 분석하는 데 사용된다.
사용 예를 들어보면 게임에서 플레이어가 상호작용하는 오브젝트가 많은 경우, 이 오브젝트들의 동작을 설계하기 위해 오토마타 이론을 사용할 수 있다. 상태와 이벤트를 정의하고 이를 이용해 각 오브젝트들의 상호작용을 모델링하면, 게임에서 예상치 못한 동작이 발생하는 것을 방지하고 보다 안정적으로 플레이를 구현할 수 있다.
또한, 오토마타 이론은 게임 인공지능(AI) 구현에도 매우 유용하다.
2.정의 : 입력을 받아 일련의 상태를 거치면서 출력을 생성하는 추상적인 기계 모델
+유한한 개수의 상태를 가진 머신이라고 할 수 있다.
이게 무엇인고.. 라고 생각할 수 있지만 간단한 사진으로 다시본다고 하면
정말 간단해 보이지 않나 ? 일반적으로 게임 캐릭터는 상태를 가진다. 뛰기, 걷기, 먹기, 공격하기 등등의 상태이다.
3.예제
원시적인 상태 제어방법을 보자.
using System;
class Program
{
enum CharacterState
{
Walking,
Running
}
static void Main(string[] args)
{
CharacterState state = CharacterState.Walking;
bool isMoving = true;
while (isMoving)
{
if (state == CharacterState.Walking)
{
Walk();
}
else if (state == CharacterState.Running)
{
Run();
}
}
}
}
음 이게 문제가 있는 코드인가요 ? 라고 물어볼 수도 있겠으나 코드에 문제는 전혀 없다.
단 캐릭터의 상태가 증가한다면 이야기는 달라질 것이다. if문의 지옥에 빠질 것이며, 메인 메서드의 길이가 늘어나서 가독성이 떨어질 것이다. 코드가 복잡해 진다는 것은 디버깅 또한 비효율적일 것이고, 그 코드를 볼 당신의 동료와 상사는 그저 눈물만 흘릴 것이다.
이 문제를 해결하기 위해서 사용하는 이론이 오토마타 이론중에서 FSM 이론이다. 오토마타 이론을 제대로 공부하려면 너무 오래걸리기도 하고, 게임에 그렇게까지 필요한 학문은 아니라고 할 수 있기 때문에 간단하게나마 알아두는 것이 나의 목표이다.
유한상태 머신의 장단점은
장점
- AI 개념을 프로그래머 외에 기획자 또는 제 3자가 쉽게 확인/설계 할 수있다.
- 직관적이다.
단점
- 확장이 힘들다. (FSM의 상태를 계속 추가하다 보면 다시 연결하기가 머리 아프다.)
이다. 너무 복잡할 경우 행동트리(행동트리는 상태를 나무 구조로 표현하여, 각 노드가 특정 동작을 수행하도록 설계된다)
로 구현이 가능하다고도 한다.
그럼 하나의 예시를 들어서 유한 상태 머신을 공부해보자.
메이플스토리를 아는가 ? 메이플스토리에서 몬스터는 이런 특징을 지닌다.
- 때리지 않을 경우 주변을 그냥 돌아다닌다.
- 때릴 경우 플레이어를 쫒아온다.
- 특정 시간이 지나거나 맵에서 플레이어가 사라지면 공격성이 사라진다.
- 특정 몬스터는 플레이어가 주변에 오면 경계한다.(메이플에 없지만 임의로 넣었다)
위의 특징을 아까 보여준 원시적인 if문으로 제어한다면 3개의 분기문이면 끝나겠지만, 우리는 멀리보는 프로그래머 이기 때문에 몬스터의 상태가 더 늘어날 것을 대비해
FSM을 이용해서 몬스터를 디자인 해보자.
정도로 구현해볼 수 있겠다.
class FiniteStateMachine
{
private State currentState;
public FiniteStateMachine()
{
currentState = State.Peaceful;
}
public void Update()
{
switch (currentState)
{
case State.Peaceful:
if (만약 플레이어가 근처에오면!)
{
SwitchState(State.Alert);
}
else if (공격 받으면!)
{
SwitchState(State.Attacking);
}
break;
case State.Alert:
if (적이 완전히 근접! || 공격 받으면!)
{
SwitchState(State.Attacking);
}
else if (플레이어가 30초동안 안보이면!)
{
SwitchState(State.Peaceful);
}
break;
case State.Attacking:
if (플레이어가 10초이상 안보이면!)
{
SwitchState(State.Alert);
}
break;
default:
break;
}
}
귀찮으니 이정도로만 구현해보겠다.
using System;
using System.Collections.Generic;
public class BaseFSM <T> where T : struct, IConvertible
{
#region State Transition
//Basic class that denote the transition between one state and another
public class StateTransition
{
public T currentState { get; set; }
public T nextState { get; set; }
//StateTransition Constructor
public StateTransition(T currentState, T nextState)
{
this.currentState = currentState;
this.nextState = nextState;
}
public override bool Equals(object obj)
{
StateTransition other = obj as StateTransition;
return other != null && this.currentState.Equals(other.currentState) && this.nextState.Equals(other.nextState);
}
}
#endregion
#region BaseFsm Implementation
protected Dictionary<StateTransition, T> transitions; //Will contain all the transitions inside the FSM
public T currentState;
public T previusState;
protected BaseFSM() {
// Throw Exception on static initialization if the given type isn't an enum.
if(!typeof (T).IsEnum)
throw new Exception(typeof(T).FullName + " is not an enum type.");
}
private T GetNext(T next)
{
StateTransition transition = new StateTransition(currentState, next);
T nextState;
if (!transitions.TryGetValue(transition, out nextState))
throw new Exception("Invalid transition: " + currentState + " -> " + next);
Console.WriteLine("Next state " + nextState);
return nextState;
}
//Used to check if the next state is reachable
public bool CanReachNext(T next) {
StateTransition transition = new StateTransition(currentState, next);
T nextState;
if (!transitions.TryGetValue(transition, out nextState)){
Console.WriteLine("Invalid transition: " + currentState + " -> " + next);
return false;
}
else {
return true;
}
}
public T MoveNext(T next)
{
previusState = currentState;
currentState = GetNext(next);
Console.WriteLine("Change state from " + previusState + " to " + currentState);
return currentState;
}
#endregion
}
위는 누군가 만들어 놓은 FSM의 Base 모듈이다. 이런걸 우린 감사합니다~ 하면서 사용하면 된다.
그대로 상속받아보자
using System;
using System.Collections.Generic;
namespace FSM
{
// State를 정의합니다.
public enum MonsterState
{
Rest,
Alert,
Attack
//최적화를 위해 Idle상태같은 것을 구현하는 것이 좋을 것 같다!
}
public class MonsterFSM : BaseFSM<CharacterState> // BaseFSM을 상속합니다.
{
public CharacterFSM() : base()
{
this.currentState = CharacterState.Idle;
// State Machine 전이를 정의합니다.
this.transitions = new Dictionary<StateTransition, CharacterState>
{
{ new StateTransition(MonsterState.Rest, MonsterState.Alert), CharacterState.Walk },
{ new StateTransition(MonsterState.Rest, MonsterState.Attack), CharacterState.Run },
{ new StateTransition(MonsterState.Alert, MonsterState.Rest), CharacterState.Idle },
{ new StateTransition(MonsterState.Alert, MonsterState.Attack), CharacterState.Attack },
{ new StateTransition(MonsterState.Attack, MonsterState.Alert), CharacterState.Attack }, //Attack에서 alert state로 전이
};
}
}
}
위의 예제를 사용해보면
MonsterFSM monster = new MonsterFSM();
Console.WriteLine(monster.GetCurrentState()); // Rest
monster.PerformTransition(MonsterState.Alert);
Console.WriteLine(monster.GetCurrentState()); // Alert
monster.PerformTransition(MonsterState.Attack);
Console.WriteLine(monster.GetCurrentState()); // Attack
monster.PerformTransition(MonsterState.Rest);
Console.WriteLine(monster.GetCurrentState()); // Attack이 유지된다. 우리는 Attack->Rest 전이를 정의하지 않았기 때문.
이렇게 사용할 수 있다.
+
만약 상태가 더 복잡해진다면
Void OnStateEnter(Monster state);
Void OnStateExecution(Monster state);
Void OnStateExit(Monster state);
을 구현하는 방식으로 만들 수 있다.
public interface IState // 인터페이스로 선언
{
void OnStateExecution();
void OnStateEnter ();
void OnStateExit ();
}
//IState를 상속받고 상태에 맞는 행동을 구현한다.
public class AttackState : IState
{
public void OnStateEnter()
{
// Attack 상태에 진입할 때 필요한 행동
}
public void OnStateExecution()
{
// Attack 상태에서 실행할 행동
}
public void OnStateExit()
{
// Attack 상태를 빠져나갈 때 필요한 행동
}
}
//그러면 몬스터는 이렇게 간단하게 사용이 가능하다.
public class Monster
{
AttackState attackState = new AttackState();
RestState restState = new RestState();
IState currentState = restState;
private void ChangeStateMethod(IState nextState)
{
currentState.OnStateExit();
nextState.OnStateEnter();
currentState = nextState;
}
}
//몬스터가 상태가 바뀔 때 메서드
Private void ChangeStateMethos(IState nextState)
{
currentState.OnStateExit();
nextState.OnStateEnter();
currentState = nextState;
}
//실행만 할 수 있게.
Void Update() {
currentState.OnStateExecution();
}
일반적으로 상태 패턴을 구현할 때, 각 상태를 클래스로 구현하는 것이 일반적입니다. 하지만 상태가 많거나 구현이 복잡할 경우 클래스 수가 많아지는 문제가 있을 수 있다. 이러한 경우 상태 클래스를 추상화하여 공통적인 부분을 부모 클래스에 구현하고 각 상태 클래스에서 이를 상속받아 구체화하는 방법을 사용한다. 또한, 상태 패턴을 구현할 때 상태 전이에 대한 로직을 각 상태 클래스에서 구현하지 않고, 상태 전이를 담당하는 별도의 클래스를 만들어 로직을 분리할 수도 있다.
비슷한 상태는 상속으로 구현할 수 있다.
class UltraAttackState : AttackState {
// 초공격적인 기능 추가
}