유한 상태 기계 (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를 추가할때 힘들것같다.
-참고한 링크
라이브러리
1번 방법에서 Switch문을 사용하지않고 대신 자동화가 감미된 라이브러리인데
사용하진 않았다.
자세한 설명은 아래 링크에서 참고
'Unity > C#' 카테고리의 다른 글
[Unity] Raycasthit.transform과 Raycasthit.collider.transform (0) | 2022.05.09 |
---|---|
[Unity] 길찾기 에셋을 활용한 Wander구현 (0) | 2022.02.24 |
[Unity, C#] Vector3 회전 (0) | 2022.01.28 |
[C#]선택적 매개변수로 Color사용 / ?? 및 ??=, ?. 연산자 (0) | 2022.01.28 |
[Unity] 카메라 장애물 회피 (0) | 2022.01.26 |