디자인패턴 - 상태패턴 : FSM(유한상태머신)
Intro
상태 패턴 이란 디자인패턴중 하나로, 객체 지향 방식으로 상태 기계를 구현하는 방법이다.
쉽게말해 객체의 내부 상태가 바뀜에따라 그 객체의 행동을 바꿀수 있도록하는 행동 디자인패턴이다.
이번 포스팅에서는 상태에 따른 동작을 제어하는 방식을 구현하기위한 FSM에 대해 알아보았다.
FSM
FSM(Finite-state Machine)은 상태와 상태간의 전환을 기반으로 동작하는 시스템이다.
예시로 남자라면 거의 다 알고있을 스타크래프트의 시즈탱크가 있다.
/*
스타크래프의 탱크는 2가지 모드가있다.
1. 일반(Tank)모드
2. 시즈(Siege)모드
탱크는 일반모드일때와 시즈모드일때 공격력, 공격속도, 사거리, 이동속도 등
능력치가 달라진다.
*/
여기서 일반모드와 시즈모드는 “상태”가 된다.
상태머신은 3가지 구성요소로 이루어져있다.
- 상태 : 시스템이 취할 수 있는 상태
- 전환 조건 : 상태 간의 전환을 결정하는 조건
- 동작 : 상태에 따른 동작
따라서, 탱크는 일반모드 상태에서 키입력을 통해 시즈모드 상태로 전환되고,
시즈모드에서는 움직이지 못하지만 더 강력한 공격을 수행할 수 있는 동작을 수행한다.
FSM은 단 하나의 상태만을 가진다.
그렇기때문에 현재 상태만 알 수 있다면 어떤 동작을 수행하려하는지 명확하게 파악할 수 있으며, 구현또한 쉽다는 장점이 있다.
상태 머신을 구현하는 방법에는 여러가지가 있다.
- if-else 또는 switch-case 를 통한 분기처리 방법
- 상태 테이블을 정의하여 활용한 방법
- 상태 패턴을 활용한 방법
분기처리 방식의 구현
if-else 또는 switch-case 문을 사용하여 구현하는것은 가장 단순한 방식이다.
public class Monster : Monobehavior
{
enum enemyState
{
Idle,
Chase,
Attack
}
private enemyState myState;
private void Start()
{
// 게임 시작시 Idle 상태
myState = enemyState.Idle;
}
private void Update()
{
switch(myState)
{
case enemyState.Idle:
// Idle 상태의 행동
break;
case enemyState.Chase:
// Chase 상태의 행동
break;
case enemyState.Attack:
// Attack 상태의 행동
break;
}
}
}
구현이 간단하고 쉬우며 직관적이다.
하지만 Monster 클래스를 상속받아서 다양한 몬스터를 구현한다고 한다면, 모든 몬스터가 같은 로직을
사용하지 않는 이상에는 적합한 방식이 아닐 수 있다.
상태패턴 방식의 구현
먼저, 공통적으로 사용될 BaseState라는 추상 클래스를 정의한다.
이 클래스는 각 상태구현을 위한 필수적인 내용을 미리 정의하는 추상 클래스다.
public abstract class BaseState
{
protected Monster _monster;
protected BaseState(Monster monster)
{
_monster = monster
}
public abstract void OnStateEnter();
public abstract void OnStateUpdate();
public abstract void OnStateExit();
}
그리고, 몬스터 AI가 지닐 수 있는 각 상태들을
BaseState를 상속받은 각각의 클래스로 구현한다.
이때 상태 클래스는 오로지 그 상태일 때 무엇을 수행해야하는것인가만 작성하면 된다.
상태간 변경에대한 책임은 Monster 클래스에게 있도록한다.
public class Idle : BaseState
{
public Idle(Monster monster) : base(monster) {}
public override void OnStateEnter()
{
}
public override void OnStateUpdate()
{
}
public override void OnStateExit()
{
}
}
public class Chase : BaseState
{
public Chase(Monster monster) : base(monster) {}
public override void OnStateEnter()
{
}
public override void OnStateUpdate()
{
}
public override void OnStateExit()
{
}
}
public class Attack : BaseState
{
public Attack(Monster monster) : base(monster) {}
public override void OnStateEnter()
{
}
public override void OnStateUpdate()
{
}
public override void OnStateExit()
{
}
}
FSM 클래스는 현재 상태 또는 상태 변경시에 호출되어야 할 메서드를 관리해주는 클래스다.
Monster 클래스는 FSM 객체를 멤버 변수로 저장하여 해당 객체를 통해 상태제어를 하도록 한다.
public class FSM
{
private BaseState curState;
public FSM(BaseState init)
{
curState = initState;
ChangeState(curState);
}
public void ChangeState(BaseState nextState)
{
// 다른 상태로만 변경가능
if(nextState == curState)
return;
// 상태를 빠져나가기위한 OnstateExit 호출
if(curState != null)
curState.OnStateExit();
// 상태변경
curState = nextState;
curState.OnStateEnter();
}
public void UpdateState()
{
if(curState != null)
curState.OnStateUpdate();
}
}
마지막으로 여러가지 몬스터를 구현하는 부분이다.
Monster 클래스를 상속받아, 개별 몬스터를 만들어주면된다.
public class Slime : Monster
{
// 슬라임이 가지는 상태
private enum State
{
Idle,
Chase,
Attack
}
private State curState;
private FSM fsm;
private void Start()
{
// 초기상태는 Idle
curState = State.Idle;
// Idle 상태인 fsm 객체 생성
fsm = new FSM(new Idle(this));
}
private void Update()
(
switch(curState)
{
case State.Idle:
// Idle 행동 작성
break;
case State.Chase:
// Chase 행동 작성
break;
case State.Attack:
// Attack 행동 작성
break;
}
fsm.UpdateState();
)
// 상태 변경
private void ChangeState(State nextState)
{
curState = nextState;
switch(curState)
{
case State.Idle:
// Idle 상태로 변경
fsm.ChangeState(new Idle(this));
break;
case State.Chase:
// Chase 상태로 변경
fsm.ChangeState(new Change(this));
break;
case State.Attack:
// Attack 상태로 변경
fsm.ChangeState(new Attack(this));
break;
}
}
}
이렇게 구현한다면, 몬스터가 가질 수 있는 상태는 고정된것이 아닌 몬스터마다 다르게된다.
예를들어 날 수 있는 몬스터는 “Fly” 상태를 추가하고,
공격하지 못하는 몬스터는 “Attack” 상태를 없애면 된다.
하지만 각 상태에대해 명확하게 정의할 수 없거나, 복잡한 AI를 구현하려는 경우
가질 수 있는 상태의 종류가 많아지고, 그렇게된다면 조건처리하는 부분이 매우 길어지게될것이다.
따라서 FSM 방식은 상태의 수가 비교적 적고, 상태에 따른 행동을 명확하게 구별할 수 있을때 사용하는것이 좋다.
댓글남기기