Unity/C#

[Unity] FSM패턴

Oniboogie 2022. 2. 22. 17:26

유한 상태 기계 (Finite State Machine)

정의

컴퓨터 프로그램을 설계할 때 쓰이는 모델이다. 컴퓨터 내에 유한한 상태를 가지는 기계가 있다고 가정하고, 컴퓨터는 오로지 하나의 상태만 갖고 있을 수 있으며 각 상태별 동작과 상태끼리의 전이에 대한 내용을 설계하게 된다.

 

유니티의 Animator처럼 한State에 머무르며 행동을하고 다른State로 전이(Transition)을 하며 행동을 변경하는 시스템이라고 생각하면 편하다.

이미지 출처 : https://boycoding.tistory.com/262

 

AI를 코딩으로 만들때 이 패턴이 유용한데,

기존에 애용하던 패턴은 Switch문을 활용한 이런식이다.

방법 1

// FSM을 활용한 몬스터 컨트롤러
public abstract class MonsterController : MonoBehaviour
{
    public enum MonsterState { Idle, Patrol, Chase, Attack, Dead }
    public MonsterState startState;[SerializeField]
    protected MonsterState currentState = MonsterState.Idle;

    public virtual void Initialize()
    {
        ChangeState(currentState);
    }

    protected virtual void Update()
    { 
        StateUpdate(currentState); 
    }

    protected virtual void FixedUpdate() 
    { 
        StateFixedUpdate(currentState); 
    }

    public virtual void ChangeState(MonsterState newState) 
    { 
        if (newState == currentState) 
            return; 
        OnStateExit(currentState); currentState = newState; 
        OnStateEnter(currentState); 
    }

    public abstract void OnStateEnter(MonsterState state); 
    
    public abstract void StateUpdate(MonsterState state); 
    
    public abstract void StateFixedUpdate(MonsterState state); 
    
    public abstract void OnStateExit(MonsterState state);
}

이렇게 베이스가될 클래스를 만들고,

 

이렇게 상속받아서 행동별 패턴을 코딩한다.

using System.Collections; 
using System.Collections.Generic; 
using UnityEngine; 
using Pathfinding; 

public class SuicideBombingMonster : MonsterController 
{ 
    public AIDestinationSetter destinationSetter; 
    public AIPath aiPath; 
    public RobotAgent currentTargetAgent; 

    public override void Initialize() 
    { 
        base.Initialize(); 
    } 

    public override void OnStateEnter(MonsterState state) 
    { 
        switch(state) 
        { 
            case MonsterState.Idle: 
                break; 
            case MonsterState.Patrol: 
                break; 
            case MonsterState.Chase:
                break; 
            case MonsterState.Attack:
                break; 
            case MonsterState.Dead:
                break; 
        } 
    } 
    
    public override void OnStateExit(MonsterState state) 
    { 
        switch (state) 
        { 
            case MonsterState.Idle: 
                break; 
            case MonsterState.Patrol: 
                break; 
            case MonsterState.Chase: 
                break; 
            case MonsterState.Attack: 
                break; 
            case MonsterState.Dead: 
                break; 
        } 
    } 
    
    public override void StateFixedUpdate(MonsterState state) 
    { 
        switch (state) 
        { 
            case MonsterState.Idle:
                break; 
            case MonsterState.Patrol:
                break;
            case MonsterState.Chase:
                break;
            case MonsterState.Attack:
                break;
            case MonsterState.Dead:
                break; 
        } 
    } 
    
    public override void StateUpdate(MonsterState state) 
    { 
        switch (state) 
        { 
            case MonsterState.Idle: 
                break; 
            case MonsterState.Patrol: 
                break; 
            case MonsterState.Chase: 
                break; 
            case MonsterState.Attack:
                break; 
            case MonsterState.Dead: 
                break; 
        } 
    } 
}

(현재 작업중인 프로젝트에서 구현중인걸 그대로 복사해와서 내용과 상관없는 코드도 일부 포함돼있다.)

 

- 장점 : 한 스크립트 내에서 모든 행동패턴을 확인할 수 있어서 이해하기 편함

- 단점 : 행동이 추가되고 또 추가돼서 덩치가 커지면 오히려 스크립트가 너무 길어져서 이해하기 불편해질 수 있음.


방법 2

써본적은 없지만 검색을하다 발견한 방법이다.

 

베이스가될 BaseState클래스를 만들고

using System.Collections; 
using System.Collections.Generic;
 using UnityEngine; 

 public class BaseState 
 {
    public string name;
    protected StateMachine stateMachine;

    public BaseState(string name, StateMachine stateMachine) 
    { 
        this.name = name; 
        this.stateMachine = stateMachine; 
    } 
    
    public virtual void Enter() 
    { 

    } 
    
    public virtual void UpdateLogic() 
    { 

    } 
    
    public virtual void UpdatePhysics() 
    { 

    } 
    
    public virtual void Exit() 
    { 

    } 
}

State의 행동을 실행하고 Transition을 시켜줄 StateMachine의 베이스 클래스도 만든다.

using System.Collections; 
using System.Collections.Generic; 
using UnityEngine; 

public class StateMachine : MonoBehaviour 
{ 
    BaseState currentState; 
    
    void Start() 
    { 
        currentState = GetInitialState(); 
        
        if (currentState != null) 
            currentState.Enter(); 
    } 
    
    void Update() 
    { 
        if (currentState != null) 
            currentState.UpdateLogic(); 
    } 
    
    void LateUpdate() 
    { 
        if (currentState != null) 
            currentState.UpdatePhysics(); 
    } 
    
    public void ChangeState(BaseState newState) 
    { 
        currentState.Exit(); 
        currentState = newState; 
        currentState.Enter(); 
    } 
    
    protected virtual BaseState GetInitialState() 
    { 
        return null; 
    } 
}

이 방법으로 Moving이라는 State를 만든다면 이렇게된다.

using System.Collections; 
using System.Collections.Generic; 
using UnityEngine; 

public class Moving : BaseState 
{ 
    private MovementSM _sm;
    private float _horizontalInput; 
    
    public Moving(MovementSM stateMachine) : base("Moving", stateMachine) 
    { 
        _sm = stateMachine; 
    } 
    
    public override void Enter() 
    { 
        base.Enter();
         _horizontalInput = 0f; 
    } 
    
    public override void UpdateLogic() 
    { 
        base.UpdateLogic(); 
        _horizontalInput = Input.GetAxis("Horizontal");

        if (Mathf.Abs(_horizontalInput) < Mathf.Epsilon) 
            stateMachine.ChangeState(_sm.idleState); 
    } 
    
    public override void UpdatePhysics() 
    { 
        base.UpdatePhysics(); 
        Vector2 vel = _sm.rigidbody.velocity; 
        vel.x = _horizontalInput * _sm.speed; 
        _sm.rigidbody.velocity = vel; 
    } 
}

이 방법은 아직 사용해보진 않았지만 장,단점을 유추해보자면

 

-장점 :

1. 한 클래스에 기능이 몰빵되지않아 관리하기 용이해보인다.

2. 확장성이 커보인다.

 

- 단점 : 각 State를 다른 스크립트에 개별 Class로 정의하다보니 Flow를 따라 해석할때나

State를 추가할때 힘들것같다.

 

 -참고한 링크

Make a basic FSM in Unity/C#
Let’s design a simple 2 states-FSM for 2D physics-based player movement!
medium.com
State Pattern using Unity
Learn all about the Finite State Machine design pattern in Unity. Then implement it to control the movement of your own character!
www.raywenderlich.com

라이브러리

GitHub - thefuntastic/Unity3d-Finite-State-Machine: An intuitive Unity3d finite state machine (FSM). Designed with an emphasis on usability, without sacrificing utility.
An intuitive Unity3d finite state machine (FSM). Designed with an emphasis on usability, without sacrificing utility. - GitHub - thefuntastic/Unity3d-Finite-State-Machine: An intuitive Unity3d fin...
github.com

1번 방법에서 Switch문을 사용하지않고 대신 자동화가 감미된 라이브러리인데

사용하진 않았다.

자세한 설명은 아래 링크에서 참고

유니티 FSM: 유한 상태 머신
유니티 FSM: 유한 상태 머신 (Finite State Machine) 유한 상태 머신(Finite State Machine, FSM)은 게임 에이전트에게 환상적인 지능을 부여하기 위한 선택 도구로 사용되어왔다. 다시 말해, 유한 상태 머신은,..
boycoding.tistory.com