This document is about: FUSION 2
SWITCH TO

유한 상태 머신

Level 4

개요

유한 상태 머신(FSM, Finite State Machine)은 Fusion을 위한 강력한 솔루션을 제공합니다. FSM을 사용하여 게임 개발자는 게임 객체가 서로 및 게임 세계와 상호 작용하는 방법을 정의하는 상태 및 전환 시스템을 만들 수 있습니다.

FSM은 시스템을 유한한 수의 상태와 상태 간의 전이로 나타내는 수학적 모델입니다. 게임 개발의 맥락에서 FSM은 플레이어, 적, NPC와 같은 게임 객체의 행동을 나타내는 데 사용될 수 있습니다.

FSM 애드온은 계층적 유한 상태 머신(HFSM) 접근 방식을 구현하지 않지만 복잡한 동작을 생성할 수 있는 방법을 제공합니다. FSM 애드온의 각 상태에는 보다 구체적인 동작 및 조건을 정의할 수 있는 하나 이상의 하위 상태 머신(자식 머신)가 포함될 수 있습니다. 자식 머신은 병렬로 실행될 수 있기 때문에 순수 HFSM 시스템의 가장 중요한 단점인 상태의 병렬 실행을 해결합니다.

게임 개발에 FSM을 사용하는 것은 더 쉬운 유지 보수, 더 나은 구성 및 복잡성 감소를 포함하여 여러 이점을 제공합니다. FSM을 사용하여 게임 개발자는 게임 상태 및 동작을 관리하기 위한 더 효율적이고 확장 가능한 시스템을 만들 수 있습니다.

개요

다운로드

버전 릴리즈 일자 다운로드
2.0.4 Aug 20, 2024 Fusion FSM 2.0.4 Build 629

특징

  • FSM 애드온은 코드 전용 유한 상태 머신 솔루션을 제공합니다
  • 동일한 게임 객체에서 여러 개의 상태 머신을 병렬로 실행할 수 있습니다
  • 하나 이상의 하위 상태 머신(자식 머신)를 사용하여 상태를 추가로 구성할 수 있습니다. 이 머신은 병렬로 실행할 수도 있습니다
  • 상태 우선순위 및 상태에 대한 CanEnter/CanExit 호출을 사용하거나 보다 전통적인 접근 방식으로 상태 간 전환 조건을 정의하여 전환 로직을 처리할 수 있습니다

기본 구조

FSM 애드온은 코드 전용 솔루션으로, 상태와 전환 로직이 전적으로 코드에 정의되어 있음을 의미합니다. 애드온의 기본 구조는 다음과 같습니다:

StateMachineController
게임 객체에 추가해야 하며 해당 객체에 할당된 하나 이상의 상태 머신을 업데이트하는 역할을 하는 스크립트입니다.

StateMachine
StateMachine은 상태 머신을 나타내는 일반 클래스로 주요 속성(예: 활성화 상태, 상태 시간)을 저장하는 데 사용되며 활성화할 상태를 제어(TryActivateStateForceActivateState를 통해)하는 데 사용할 수 있습니다.

State
상태는 특정 동작을 나타내며 게임 객체의 계층에 추가되는 NetworkBehaviour(StateBehaviour에서 상속됨)이거나 일반 클래스(State에서 상속됨)일 수 있습니다.

노트: 사용자는 상태 머신의 특정 기능에 필요한 추가 속성을 추가하기 위해 공통 기본 클래스의 상태를 생성하는 것이 좋습니다. 예를 들어, 공통 기본 클래스 EnemyAIStateEnemyAI 컴포넌트에 대한 참조가 있을 수 있습니다. 자세한 내용은 상태 확장 섹션을 참조하십시오.

시작 방법

FSM 추가 기능을 사용하려면 다음 단계를 수행합니다:

1) 네트워크 객체에 StateMachineController 스크립트를 추가합니다

개요

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...");
    }
}
개요

3) 상태 머신을 보유할 모든 NetworkBehaviourIStateMachineOwner 인터페이스를 구현합니다. 우리의 경우는 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);
    }
}

StateMachineControllerIStateMachineOwner 인터페이스를 구현하는 모든 컴포넌트를 찾아 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 플래그를 설정하면 해당 상태의 CanExitStateCanEnterState가 체크되지 않습니다.

C#

_idleState.AddTransition(_attackState, CanStartAttack, true);

노트: 상태 우선순위는 전환에 대해 확인되지 않습니다.

전환 상태에 접근해야 할 경우 currentStatetargetState를 파라미터로 사용할 수 있는 대체 AddTransition 메소드가 있습니다.

C#

private static bool CanStartAttack(StateBehaviour currentState, StateBehaviour targetState)
{
    return currentState.Machine.StateTime > 2f;
}

상태 확장하기

사용자는 상태 머신의 특정 기능에 필요한 추가 속성을 추가하기 위해 사용자 정의 StateStateBehaviour기본 클래스를 생성하는 것이 좋습니다. 예를 들어, 일반적인 기본 클래스 PlayerStateBehaviourAnimancerComponentCharacterController 컴포넌트를 참조합니다. 모든 플레이어 상태 동작은 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, OnFixedUpdateOnExitState 메소드 또는 정의된 전환에 의해 제어되어야 합니다.

자식 머신은 상위 상태가 활성화되면 업데이트됩니다. 다른 모든 방법으로 표준 상태 시스템과 동일합니다. 자식 머신 상태는 계층의 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 등)를 호출하지 마십시오.
  • 비주얼은 항상 렌더 메소드를 사용하십시오. OnEnterStateOnExitState는 프록시에서 호출되지 않습니다.
  • 상태 머신 실행은 IsPaused 속성을 설정하여 일시 중지할 수 있습니다.
  • State 플레인 클래스를 사용할 때는 필요한 네트워크 데이터를 저장하기 위해 상위 StateBehaviour에 대한 참조를 저장하는 것을 고려해야 합니다. 사용자 정의 데이터를 상태에 직접 저장하는 것이 가능하지만(아래 장 참조), 그렇게 하는 것이 훨씬 편리하지 않습니다.
  • 동일한 객체에서 여러 개의 상태 시스템이 병렬로 실행되는 것은 드물지 않습니다. 여러 개의 상태 머신을 실행하는 데는 일반적으로 두 가지 이유가 있습니다:
    • 인공지능을 위한 머신과 애니메이션 및 비주얼을 위한 머신과 같이 여러 영역에서 상태 머신 로직이 필요합니다.
    • 공격, 순찰, 검색과 같은 행동을 실행하는 일반 AI 머신 및 달리기, 스트래핑, 점프 패드 사용, 격차 극복 등의 이동 행동을 실행하는 이동 AI 머신과 같은 특정 로직은 병렬 실행의 이점을 얻습니다.

디버깅

런타임에 StateMachineController 컴포넌트가 있는 게임 객체를 선택하면 컴포넌트 인스펙터에 디버그 정보가 표시됩니다.

개요

상태 머신 동작을 디버깅하려면 자세한 로깅을 사용할 수 있습니다. 상태 머신 컨트롤러 검사기에서 로깅을 사용하도록 설정할 수 있으며, 이를 통해 수집된 모든 상태 머신을 기록할 수 있습니다. 보다 세분화된 접근 방식이 필요한 경우 사용자는 특정 상태 머신에 대해 로깅을 설정하거나 해제할 수 있습니다.

C#

_stateMachine.EnableLogging = true;
개요

사용자 정의 상태 데이터

숙련한 사용자의 경우 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에서 상속됨)에 대한 참조를 상태에 저장하고 필요한 네트워크 데이터를 저장하는 것이 좋습니다(상태 확장 섹션의 코드 참조).

Back to top