3 분 소요

Intro

상태 패턴 이란 디자인패턴중 하나로, 객체 지향 방식으로 상태 기계를 구현하는 방법이다.

쉽게말해 객체의 내부 상태가 바뀜에따라 그 객체의 행동을 바꿀수 있도록하는 행동 디자인패턴이다.

이번 포스팅에서는 상태에 따른 동작을 제어하는 방식을 구현하기위한 FSM에 대해 알아보았다.

FSM

FSM(Finite-state Machine)은 상태와 상태간의 전환을 기반으로 동작하는 시스템이다.

예시로 남자라면 거의 다 알고있을 스타크래프트의 시즈탱크가 있다.

/*
                스타크래프의 탱크는 2가지 모드가있다.

            1. 일반(Tank)모드
            2. 시즈(Siege)모드

            탱크는 일반모드일때와 시즈모드일때 공격력, 공격속도, 사거리, 이동속도 등
            능력치가 달라진다.
*/

여기서 일반모드와 시즈모드는 “상태”가 된다.

상태머신은 3가지 구성요소로 이루어져있다.

  1. 상태 : 시스템이 취할 수 있는 상태
  2. 전환 조건 : 상태 간의 전환을 결정하는 조건
  3. 동작 : 상태에 따른 동작

따라서, 탱크는 일반모드 상태에서 키입력을 통해 시즈모드 상태로 전환되고,

시즈모드에서는 움직이지 못하지만 더 강력한 공격을 수행할 수 있는 동작을 수행한다.



FSM은 단 하나의 상태만을 가진다.

그렇기때문에 현재 상태만 알 수 있다면 어떤 동작을 수행하려하는지 명확하게 파악할 수 있으며, 구현또한 쉽다는 장점이 있다.


상태 머신을 구현하는 방법에는 여러가지가 있다.

  1. if-else 또는 switch-case 를 통한 분기처리 방법
  2. 상태 테이블을 정의하여 활용한 방법
  3. 상태 패턴을 활용한 방법

분기처리 방식의 구현

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 방식은 상태의 수가 비교적 적고, 상태에 따른 행동을 명확하게 구별할 수 있을때 사용하는것이 좋다.

태그: ,

카테고리:

업데이트:

댓글남기기