무한 상태 머신
![Level 4](/v2/img/docs/levels/level04-advanced_1.5x.png)
개요
무한 상태 머신(FSM, Finite State Machine)은 Fusion을 위한 강력한 솔루션을 제공합니다. FSM을 사용하여 게임 개발자는 게임 객체가 서로 및 게임 세계와 상호 작용하는 방법을 정의하는 상태 및 전환 시스템을 만들 수 있습니다.
FSM은 시스템을 유한한 수의 상태와 상태 간의 전이로 나타내는 수학적 모델입니다. 게임 개발의 맥락에서 FSM은 플레이어, 적, NPC와 같은 게임 객체의 행동을 나타내는 데 사용될 수 있습니다.
FSM 애드온은 계층적 유한 상태 머신(HFSM) 접근 방식을 구현하지 않지만 복잡한 동작을 생성할 수 있는 방법을 제공합니다. FSM 애드온의 각 상태에는 보다 구체적인 동작 및 조건을 정의할 수 있는 하나 이상의 하위 상태 머신(자식 머신)가 포함될 수 있습니다. 자식 머신은 병렬로 실행될 수 있기 때문에 순수 HFSM 시스템의 가장 중요한 단점인 상태의 병렬 실행을 해결합니다.
게임 개발에 FSM을 사용하는 것은 더 쉬운 유지 보수, 더 나은 구성 및 복잡성 감소를 포함하여 여러 이점을 제공합니다. FSM을 사용하여 게임 개발자는 게임 상태 및 동작을 관리하기 위한 더 효율적이고 확장 가능한 시스템을 만들 수 있습니다.
![개요](/docs/img/fusion/addons/fsm/fsm-overview.jpg)
다운로드
버전 | 릴리즈 일자 | 다운로드 | |
---|---|---|---|
1.2.0 | Jul 11, 2023 | Fusion FSM 1.2.0 Build 210 |
특징
- FSM 애드온은 코드 전용 유한 상태 머신 솔루션을 제공합니다
- 동일한 게임 객체에서 여러 개의 상태 머신을 병렬로 실행할 수 있습니다
- 하나 이상의 하위 상태 머신(자식 머신)를 사용하여 상태를 추가로 구성할 수 있습니다. 이 머신은 병렬로 실행할 수도 있습니다
- 상태 우선순위 및 상태에 대한 CanEnter/CanExit 호출을 사용하거나 보다 전통적인 접근 방식으로 상태 간 전환 조건을 정의하여 전환 로직을 처리할 수 있습니다
기본 구조
FSM 애드온은 코드 전용 솔루션으로, 상태와 전환 로직이 전적으로 코드에 정의되어 있음을 의미합니다. 애드온의 기본 구조는 다음과 같습니다:
StateMachineController
게임 객체에 추가해야 하며 해당 객체에 할당된 하나 이상의 상태 머신을 업데이트하는 역할을 하는 스크립트입니다.
StateMachine
StateMachine
은 상태 머신을 나타내는 일반 클래스로 주요 속성(예: 활성화 상태, 상태 시간)을 저장하는 데 사용되며 활성화할 상태를 제어(TryActivateState
및 ForceActivateState
를 통해)하는 데 사용할 수 있습니다.
State
상태는 특정 동작을 나타내며 게임 객체의 계층에 추가되는 NetworkBehaviour
(StateBehaviour
에서 상속됨)이거나 일반 클래스(State
에서 상속됨)일 수 있습니다.
노트: 사용자는 상태 머신의 특정 기능에 필요한 추가 속성을 추가하기 위해 공통 기본 클래스의 상태를 생성하는 것이 좋습니다. 예를 들어, 공통 기본 클래스 EnemyAIState
은 Enemy
및 AI
컴포넌트에 대한 참조가 있을 수 있습니다. 자세한 내용은 상태 확장 섹션을 참조하십시오.
시작 방법
FSM 추가 기능을 사용하려면 다음 단계를 수행합니다:
1) 네트워크 객체에 StateMachineController
스크립트를 추가합니다
![개요](/docs/img/fusion/addons/fsm/state-machine-controller.png)
StateMachineController
는 네트워크 객체 계층의 모든 상태 시스템에 대한 네트워크 데이터를 업데이트하고 동기화하는 역할을 합니다.
2) 상태를 나타내는 상태 동작 스크립트를 만들어 네트워크 객체 계층의 객체에 할당합니다.
C#
public class IdleBehaviour : StateBehaviour
{
protected override bool CanExitState(StateBehaviour nextState)
{
// Wait at least 3 seconds before idling finishes
return Machine.StateTime > 3f;
}
protected override void OnEnterStateRender()
{
Debug.Log("Idling...");
}
}
C#
public class AttackBehaviour : StateBehaviour
{
protected override void OnFixedUpdate()
{
if (Machine.StateTime > 1f)
{
// Attack finished, deactivate
Machine.TryDeactivateState(StateId);
}
}
protected override void OnEnterStateRender()
{
Debug.Log("Attacking...");
}
}
![개요](/docs/img/fusion/addons/fsm/states.png)
3) 상태 머신을 보유할 모든 NetworkBehaviour
에 IStateMachineOwner
인터페이스를 구현합니다. 우리의 경우는 EnemyAI
클래스입니다
C#
RequireComponent(typeof(StateMachineController))]
public class EnemyAI : NetworkBehaviour, IStateMachineOwner
{
[SerializeField]
private IdleBehaviour _idleState;
[SerializeField]
private AttackBehaviour _attackState;
private StateMachine<StateBehaviour> _enemyAI;
void IStateMachineOwner.CollectStateMachines(List<IStateMachine> stateMachines)
{
_enemyAI = new StateMachine<StateBehaviour>("Enemy AI", _idleState, _attackState);
stateMachines.Add(_enemyAI);
}
}
StateMachineController
는 IStateMachineOwner
인터페이스를 구현하는 모든 컴포넌트를 찾아 CollectStateMachines
함수를 호출하여 업데이트가 필요한 모든 상태 머신을 얻습니다.
이 호출 중에는 상태 머신 자체가 모든 관련 상태로 구성됩니다.
4) 이제부터 상태 머신이 업데이트됩니다. FixedUpdateNetwork
호출에서 상태 머신 제어를 시작할 수 있습니다.
C#
RequireComponent(typeof(StateMachineController))]
public class EnemyAI : NetworkBehaviour, IStateMachineOwner
{
[SerializeField]
private IdleBehaviour _idleState;
[SerializeField]
private AttackBehaviour _attackState;
private StateMachine<StateBehaviour> _enemyAI;
void IStateMachineOwner.CollectStateMachines(List<IStateMachine> stateMachines)
{
_enemyAI = new StateMachine<StateBehaviour>("Enemy AI", _idleState, _attackState);
stateMachines.Add(_enemyAI);
}
public override void FixedUpdateNetwork()
{
_enemyAI.TryActivateState(_attackState);
}
}
상태 로직
FSM 추가 기능의 상태는 일반적인 Fusion 접근법을 따릅니다. 일부 메소드는 네트워크 상태를 조작하는 데 사용됩니다(FixedUpdateNetwork
와 동일). 이러한 메소드들은 입력 및 상태 권한에만 사용되어야 합니다.
CanEnterState
- 상태를 입력할 수 있는지를 결정합니다
CanExitState
- 상태를 나올 수 있는지를 결정합니다
OnEnterState
- 상태가 활성화되면 트리거됨
OnFixedUpdate
- 상태의 FixedUpdateNetwork
와 동일
OnExitState
- 상태가 비활성화되면 트리거 됨
일부 방법은 비주얼에만 사용되며 모든 클라이언트에서 호출됩니다:
OnEnterStateRender
- 예를 들어, 점프 상태에 진입할 때 점프 파티클과 소리를 재생하는 데 사용됩니다
OnRender
- 애니메이션 파라미터와 같은 비주얼을 업데이트하는 데 사용됩니다
OnExitStateRender
- 예를 들어, 실행 중인 파티클 효과를 중지하는 데 사용됩니다
전환 로직
한 상태에서 다른 상태로 전환하는 데는 두 가지 방법이 있습니다:
1. StateMachine 제어
StateMachine
에는 다음 상태를 설정하는 데 사용할 수 있는 기본 메소드가 있습니다:
TryActivateState
상태 머신은 현재 상태의 CanExitState
와 다음 상태의 CanEnterState
를 확인합니다. 둘 다 통과하면 상태가 전환됩니다.
노트: 현재 상태에서 "종료 시 우선순위 확인"이 활성화되면 새 상태의 우선순위가 현재 상태와 동일하거나 높을 때만 상태가 전환됩니다.
TryDeactivateState
TryActivateState(defaultState)
를 호출하는 것과 같습니다. 기본 상태는 상태 머신을 생성할 때 상태 배열에서 첫 번째로 전달되는 상태입니다.
ForceActivateState/ForceDeactivateState
CanEnterState
, CanExitState
및 상태 우선순위가 확인되지 않는 것을 제외하고 TryActivateState
/TryDeactivateState
와 유사합니다.
TryToggleState/ForceToggleState
이 메서드는 상태를 켜고 끕니다(비활성화). 스위치를 끄면 기본 상태가 활성화됩니다.
올바른 상태 우선순위 및 진입/종료 조건을 설정하여 전체 상태 머신을 제어할 수 있습니다. 다음은 Animancer 애니메이션을 제어하는 시스템의 예입니다:
C#
void IStateMachineOwner.CollectStateMachines(List<IStateMachine> stateMachines)
{
var playerContext = PreparePlayerContext();
var fullBodyStates = _fullBodyStatesRoot.GetComponentsInChildren<PlayerStateBehaviour>(true);
_fullBodyAnimationMachine = new PlayerBehaviourMachine("Full Body", playerContext, fullBodyStates);
stateMachines.Add(_fullBodyAnimationMachine);
var weaponStates = _weaponStatesRoot.GetComponentsInChildren<PlayerStateBehaviour>(true);
_weaponAnimationMachine = new PlayerBehaviourMachine("Weapon", playerContext, weaponStates);
stateMachines.Add(_weaponAnimationMachine);
}
public override void FixedUpdateNetwork()
{
if (_player.IsDead == true)
{
_fullBodyAnimationMachine.ForceActivateState<PlayerDeathState>();
return;
}
_weaponAnimationMachine.TryToggleState<PlayerArmWeaponState>(_player.InCombat);
if (_movement.IsGrounded == false)
{
_fullBodyAnimationMachine.TryActivateState<PlayerAirborneState>();
}
_fullBodyAnimationMachine.TryToggleState<PlayerReviveState>(_revive.IsReviving);
_fullBodyAnimationMachine.TryToggleState<PlayerInteractionState>(_interaction.IsInteracting);
_fullBodyAnimationMachine.TryToggleState<PlayerAbilityState>(_abilities.AbilityActive);
// When other states are deactivated and default (Locomotion) is set, check if it shouldn't be Idle instead
_fullBodyAnimationMachine.TryToggleState<PlayerIdleState>(_movement.HorizontalSpeed < 0.1f);
}
2. 전환
전환은 CollectStateMachines
호출 중 상태에 할당된 콜백 래퍼입니다. 상태가 활성화되면 FixedUpdateNetwork
호출 중에 상태에서 모든 전환이 확인됩니다. 전환은 여러 상태에서 다른 상태로의 전환 로직이 동일하거나 여러 상태 머신에서 동일한 전환 로직이 사용될 때 특히 유용합니다. 이렇게 전환 로직은 한 번만 작성되고 어떤 횟수의 상태에 대해서도 사용됩니다.
C#
void IStateMachineOwner.CollectStateMachines(List<IStateMachine> stateMachines)
{
_enemyAI = new StateMachine<StateBehaviour>("Enemy AI", _idleState, _attackState);
stateMachines.Add(_enemyAI);
_idleState.AddTransition(_attackState, CanStartAttack);
}
private bool CanStartAttack()
{
if (_weapons.HasWeapon == false)
return false;
return HasEnemy();
}
전환을 위해 IsForced
플래그를 설정하면 해당 상태의 CanExitState
및 CanEnterState
가 체크되지 않습니다.
C#
_idleState.AddTransition(_attackState, CanStartAttack, true);
노트: 상태 우선순위는 전환에 대해 확인되지 않습니다.
전환 상태에 접근해야 할 경우 currentState
및 targetState
를 파라미터로 사용할 수 있는 대체 AddTransition
메소드가 있습니다.
C#
private static bool CanStartAttack(StateBehaviour currentState, StateBehaviour targetState)
{
return currentState.Machine.StateTime > 2f;
}
상태 확장하기
사용자는 상태 머신의 특정 기능에 필요한 추가 속성을 추가하기 위해 사용자 정의 State
및 StateBehaviour
기본 클래스를 생성하는 것이 좋습니다. 예를 들어, 일반적인 기본 클래스 PlayerStateBehaviour
는 AnimancerComponent
및 CharacterController
컴포넌트를 참조합니다. 모든 플레이어 상태 동작은 PlayerStateBehaviour
기본 클래스에서 상속됩니다.
C#
// Player behaviour that should be placed on GameObject in Player hierarchy
// - inherits from standard NetworkBehaviour so standard networked properties can be used
public class PlayerStateBehaviour : StateBehaviour<PlayerStateBehaviour>
{
[HideInInspector]
public CharacterController Controller;
[HideInInspector]
public AnimancerComponent Animancer;
}
// Plain class to be used as potential sub-states
// - does not inherit from NetworkBehaviour, create reference for parent PlayerStateBehaviour and store networked properties there
[Serializable]
public class PlayerState : State<PlayerState>
{
[HideInInspector]
public PlayerStateBehaviour ParentState;
[HideInInspector]
public AnimancerComponent Animancer;
}
// FSM machine to operate with PlayerStateBehaviours
public class PlayerBehaviourMachine : StateMachine<PlayerStateBehaviour>
{
public PlayerBehaviourMachine(string name, CharacterController controller, AnimancerComponent animancer, params PlayerStateBehaviour[] states) : base(name, states)
{
for (int i = 0; i < states.Length; i++)
{
var state = states[i];
state.Controller = controller;
state.Animancer = animancer;
}
}
}
// FSM machine to operate with PlayerStates plain classes, can be used as child machine
public class PlayerStateMachine : StateMachine<PlayerState>
{
public PlayerStateMachine(string name, PlayerStateBehaviour parentState, AnimancerComponent animancer, params PlayerState[] states) : base(name, states)
{
for (int i = 0; i < states.Length; i++)
{
var state = states[i];
state.ParentState = parentState;
state.Animancer = animancer;
}
}
}
자식 머신
이 추가 기능은 자식 상태 머신을 사용하여 매우 복잡한 시나리오도 수용할 수 있습니다. 자식 시스템은 모든 상태에서 OnCollectChildStateMachines
호출에 수집됩니다.
자식 머신은 상위 상태의 OnEnterState
, OnFixedUpdate
및 OnExitState
메소드 또는 정의된 전환에 의해 제어되어야 합니다.
자식 머신은 상위 상태가 활성화되면 업데이트됩니다. 다른 모든 방법으로 표준 상태 시스템과 동일합니다. 자식 머신 상태는 계층의 NetworkBehaviour
(StateBehaviour
) 또는 일반 클래스(State
) 중 하나일 수 있습니다. 일반 클래스를 사용하는 경우 필요한 설정을 위해 상위 상태 동작으로 직렬화하는 것이 편리합니다:
C#
public class AdvancedAttackBehaviour : StateBehaviour
{
[SerializeField]
private PrepareState _prepareState;
[SerializeField]
private AttackState _attackState;
[SerializeField]
private RecoverState _recoverState;
private StateMachine<State> _attackMachine;
protected override void OnCollectChildStateMachines(List<IStateMachine> stateMachines)
{
_attackMachine = new StateMachine<State>("Attack Machine", _prepareState, _attackState, _recoverState);
stateMachines.Add(_attackMachine);
}
protected override void OnEnterState()
{
// Reset to Prepare state
_attackMachine.ForceActivateState(_prepareState, true);
}
protected override void OnFixedUpdate()
{
if (_recoverState.IsFinished == true)
{
// Attack finished, deactivate
Machine.TryDeactivateState(StateId);
}
}
// STATES
[Serializable]
public class PrepareState : State
{
protected override void OnFixedUpdate()
{
if (Machine.StateTime > 1f)
{
Machine.TryActivateState<AttackState>();
}
}
protected override void OnEnterStateRender()
{
Debug.Log("Preparing attack...");
}
}
[Serializable]
public class AttackState : State
{
protected override void OnFixedUpdate()
{
if (Machine.StateTime > 0.5f)
{
Machine.TryActivateState<RecoverState>();
}
}
protected override void OnEnterStateRender()
{
Debug.Log("Attacking...");
}
}
[Serializable]
public class RecoverState : State
{
public bool IsFinished => Machine.ActiveStateId == StateId && Machine.StateTime > 2f;
protected override void OnEnterStateRender()
{
Debug.Log("Recovering from attack...");
}
}
}
모범 사례
- Render 메소드에서 네트워크 상태를 수정하거나 상태 머신 제어 메서드(
TryActivateState
등)를 호출하지 마십시오. - 비주얼은 항상 렌더 메소드를 사용하십시오.
OnEnterState
와OnExitState
는 프록시에서 호출되지 않습니다. - 상태 머신 실행은
IsPaused
속성을 설정하여 일시 중지할 수 있습니다. State
플레인 클래스를 사용할 때는 필요한 네트워크 데이터를 저장하기 위해 상위StateBehaviour
에 대한 참조를 저장하는 것을 고려해야 합니다. 사용자 정의 데이터를 상태에 직접 저장하는 것이 가능하지만(아래 장 참조), 그렇게 하는 것이 훨씬 편리하지 않습니다.- 동일한 객체에서 여러 개의 상태 시스템이 병렬로 실행되는 것은 드물지 않습니다. 여러 개의 상태 머신을 실행하는 데는 일반적으로 두 가지 이유가 있습니다:
- 인공지능을 위한 머신과 애니메이션 및 비주얼을 위한 머신과 같이 여러 영역에서 상태 머신 로직이 필요합니다.
- 공격, 순찰, 검색과 같은 행동을 실행하는 일반 AI 머신 및 달리기, 스트래핑, 점프 패드 사용, 격차 극복 등의 이동 행동을 실행하는 이동 AI 머신과 같은 특정 로직은 병렬 실행의 이점을 얻습니다.
디버깅
런타임에 StateMachineController
컴포넌트가 있는 게임 객체를 선택하면 컴포넌트 인스펙터에 디버그 정보가 표시됩니다.
![개요](/docs/img/fusion/addons/fsm/state-machine-debug-info.gif)
상태 머신 동작을 디버깅하려면 자세한 로깅을 사용할 수 있습니다. 상태 머신 컨트롤러 검사기에서 로깅을 사용하도록 설정할 수 있으며, 이를 통해 수집된 모든 상태 머신을 기록할 수 있습니다. 보다 세분화된 접근 방식이 필요한 경우 사용자는 특정 상태 머신에 대해 로깅을 설정하거나 해제할 수 있습니다.
C#
_stateMachine.EnableLogging = true;
![개요](/docs/img/fusion/addons/fsm/state-machine-logging.png)
사용자 정의 상태 데이터
숙련한 사용자의 경우 State
에서 상속할 때 사용자 지정 네트워크 데이터를 정의하는 옵션이 있습니다. 이를 위해서는 GetNetworkDataWordCount
, WriteNetworkData
, 및 ReadNetworkData
메소드를 오버라이드 해야 합니다.
C#
public unsafe class AttackState : State
{
public int AttackCount;
protected override void OnEnterState()
{
AttackCount++;
}
protected override int GetNetworkDataWordCount() => 1;
protected override void ReadNetworkData(int* ptr)
{
AttackCount = *ptr;
}
protected override void WriteNetworkData(int* ptr)
{
*ptr = AttackCount;
}
}
그러나 대부분의 경우 상위 StateBehaviour
(표준 NetworkBehaviour
에서 상속됨)에 대한 참조를 상태에 저장하고 필요한 네트워크 데이터를 저장하는 것이 좋습니다(상태 확장 섹션의 코드 참조).