유한상태기계

FSM이란 인공지능 기법중 하나로 유한한개수의 상태를 가지는 추상기계이다.

유니티에선 몬스터 AI나 플레이어의 상태를 구현할 때 유한상태기계가 많이 쓰이는데 간단하게 설명하자면 오로지 하나의 상태만 가지는 기계로 생각하면 편하다.

클래스를 제네릭화 하여 단체행동을 관리하기에도 편하다

현재 상태를 나타내다 특정한 사건에 의해서 다른 상태로 전이가 가능하며 현재 상태와 전이된 상태의 조건집합으로 정의된다.

fsm은 상태로 행동을 정의하기 때문에 코드수정, 가독성 측면에서 높은 활용성을 보인다.

사람을 하나의 fsm기계로 본다면 걷다, 자다, 먹다 등 한나의 상태로 볼 수 있으며 전이조건은 배고픔, 피곤함 등으로 설정하여 추상적으로 그릴 수 있다.

하지만 걸으면서 먹기, 자면서 움직이기? 등.. 행동이 여러 방면으로 나타날 수 있기때문에 추상적인 행동으로 나타내기 적합하다 만약 해당 행동을 제어하고 싶다면 뒤에서 나오는 블립, 전역상태와 같은 추가적인 작업이 필요할 수 있다..

ex) 컴퓨터를 끄다/키다, 리모컨 등등.. 같은 성격을 지닌다.

유니티에서 사용되는 fsm모델이라고 해서 모두 같은 형태인것이 아니라 프로그래머가 어떻게 정의하고 구현에 따라서 달라진다.

프로그래머가 프로그램의 전체적인 틀에 따라서 유연하게 제작이 필요하다..!

// 현재 캐릭터의 상태를 관리
IEnumerator CheckMonsterState()
{
    while(!isDie)
    {
        yield return new WaitForSeconds(0.3f);

        if (state == State.DIE)
            yield break;
        float distance = Vector2.Distance(_target.position, _monster.position);
        if (distance <= attackDist)
            state = State.ATTACK;
        else if (distance <= monsterStatus.attackRange)
            state = State.TRACE;
        else
            state = State.IDLE;
    }
}

// 상태에 따른 행동 관리
IEnumerator MonsterAction()
{
    while (!isDie)
    {
        switch (state)
        {
            case State.IDLE:
                _moveDir = Random.Range(-1, 2);
                yield return new WaitForSeconds(1f);
                break;
            case State.TRACE:
                _moveDir = (_target.position.x - _monster.position.x) >= 0 ? 1 : -1;
                break;
            //공격

            //죽음
        }
        yield return new WaitForSeconds(0.3f);
    }
}

플레이어와의 거리를 구하여 추격, 공격, 대기와 같은 상태로 전이한다.

CheckMonsterState해당 오브젝트의 전이단계를 설정하여 상태를 전이하고 MonsterAction는 해당 상태의 행동을 정의한다.

2D의 몬스터에 적용된 fsm모델이라 매우 간단하게 작성할 수 있어서 해당 형태를 채택했지만 만약 몬스터의 스킬이나 추가적인 상태들이 생긴다면 case문이 늘어나고 조건들이 더욱 다양해지면서 코드의 가독성이 하락할것이다.

가독성뿐만 아니라 서로의 영향을 주고 받기 때문에 작성도중에 길을 잃어버리기 쉬움..

이러한 형태의 fsm은 구현이 매우 간단하지만 상태전이과정이 복잡해지면 코드자체도 매우 복잡해진다.
확장성도 떨어지게 된다.

이러한 단점을 해결하기 위해선 애초에 각 상태를 클래스단위로 구분하여 유연함을 더해줄 수 있다.

각 상태를 코루틴으로 분기하여서 작성하면 해당 코루틴에서 의 작동을 서브루틴으로 작성할 수 있다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum PlayerState { Idle, Walk, Run, Attack}
public class fsmTest : MonoBehaviour
{
    PlayerState playerState;
    private void Awake()
    {
        ChangeState(PlayerState.Idle);
    }
    private void Update()
    {
        if (Input.GetKeyDown("a")) ChangeState(PlayerState.Walk);
        else if (Input.GetKeyDown("s")) ChangeState(PlayerState.Run);
        else if (Input.GetKeyDown("d")) ChangeState(PlayerState.Attack);
    }
    private void ChangeState(PlayerState newState)
    {
        StopCoroutine(playerState.ToString());
        playerState = newState;
        StartCoroutine(playerState.ToString());
    }
    private IEnumerator Idle()
    {
        Debug.Log("기본동작 설정");
        while (true)
        {
            Debug.Log("플레이어 대기중..");
            yield return null;
        }
    }
    private IEnumerator Walk()
    {
        Debug.Log("걷기 설정");
        while (true)
        {
            Debug.Log("플레이어 걸어가는중..");
            yield return null;
            if(Input.GetKeyDown("q"))
                break;
        }
        Debug.Log("걷기 종료");
    }
    private IEnumerator Run()
    {
        Debug.Log("뛰기 설정");
        while (true)
        {
            Debug.Log("플레이어 뛰어가는중..");
            yield return null;
        }
    }
    private IEnumerator Attack()
    {
        Debug.Log("때리기 설정");
        while (true)
        {
            Debug.Log("플레이어 공격중..");
            yield return null;
        }
    }
}

플레이어로 부터 키 입력을 받아서 상태를 제어하는 fsm모델이다.

각 상태를 코루틴으로 구분하여 작성할 수 있으며 각 상태 전이를 enum과 이름을 동일하게 하여 tostring으로 변환하여 전이 가능하다.

하지만 이 코드 또한 행동 단위를 늘리기에는 편하겠지만 다른 클래스로 정의되거나 추가해야하는 부분성에서는 부족한 코드이다.

++ 현재 play스크립트 내부에 존재할 때 위와같이 행동 정의부분이 같이 존재한다면 매우 복잡해질 것..

private void ChangeState(PlayerState newState)메서드 형태는 기본적인 상태전환코드로 애니메이션이나 다른 행위를 변경할 때 유용하게 변형하여 사용 가능..!

그렇다면 객체지향답게 속성과 메서드를 분리하여 작성해보면 조금 더 쉽고 편하게 접근이 가능하다.

++ 제네릭 활용

다형성, 캡슐화의 목적을 두고

1. entity 기본 base모델

가장 설계하기 힘들다고 느끼는 가장 기초가 되는 base모델 설계이다.

base모델이기 때문에 abstract키워드를 통해 추상화 클래스로 관리하여 동적 바인딩이나 다형성의 목적을 두는것이 좋다.

만약 몬스터라고 한다면 해당 객체에 대한 넘버링이 되어야 하기 때문에 static변수로 클래스에 존재한다면 객체 생성 시 ++해줌으로 전체적인 넘버링이 가능해질 수 있다.

private static int m_ID = 0;

가장 기본이되는 넘버링이나 이름, 색상등의 속성을 가질 수 있다.

너무 많이 넣어두지 말고 공통적으로 필요한 부분만 도려내어 작성할 것

메서드는 초기화 메서드를 virtual키워드로 작성하여 기본 초기화 메서드를 만들어 두고 오버라이딩한뒤 기본 초기화 메서드를 호출하는 식으로 코드의 중복성을 줄일 스 있다.

가장 기본이 되는 Update의 합수도 모든 객체가 필요하다면 abstract로 생성하여 구현을 강제할 수 있고 필요한 기본적인 메서드 또한 넣어둘 수 있다.

2. 기본 state

객체의 행동 패턴을 3단계로 나누어 만든다면 행동을 시작하기전 1회 호출되는 enter

이후 행동 중 계속 호출되는 Excute(Update)

행동 이후 종료될때 호출되는 Exit로 나누어서 작성할 수 있다.

이러한 행동양식이 정해졌다면 State클래스를 생성하여 위와 같은 기본행동들을 추상클래스로 등록하여 객체에서 레퍼런스 변수로 관리할 수 있도록 한다.

State클래스를 상속받은 클래스는 3가지 기본행동양식의 구현이 강제되기 때문에 하나의 행동패턴으로 정의할 수 있다.

좀 더 쉽게 관리할려면 하나의 스크립트를 추가적으로 생성하고(행동양식)객체 맞게 namespace로 묶어서 객체에 구현된 행동 양식들의 이름으로 클래스들을 생성하고 State를 상속받게 되면 행동하나가 클래스로 생성되게 된다.

  • ++ 제네릭활용: 일반화프로그래밍을 좀 더 활용한다면 쉽게 관리가 가능해진다.
public abstract class State<T> where T : class
{
    public abstract void Enter(T entity);

    public abstract void Execute(T entity);

    public abstract void Exit(T entity);
}

3. 실체화 클래스 설계

위에서 작성한 base를 상속받아서 작성하게되는 entity 클래스

객체 추가적으로 필요한 속성을 정의하거나 메서드를 오버라이딩하여 사용한다.

내부에서는 State변수로 행동 양식을 관리할 수 있다.
생성한 행동양식을 State배열로 저장하여 현재 상태를 변경하며 관리

추가적으로 현재상태를 관리하는 또다른 스크립트를 만들어 해당 스크립트로 상태전이를 관리하는 것을 추천한다.

클래스내부에는 정말 필요한 업데이트부분을 제외하고 클래스로 구분하여 작성..!

태그: ,

카테고리:

업데이트:

댓글남기기