This document is about: FUSION 2
SWITCH TO

Finite State Machine

Level 4
level advanced

{% toc NoHeader %}

Overview

概要

FusionのFinite State Machine(有限ステートマシン:FSM)アドオンは、ゲームのステートとビヘイビアを管理する強力なソリューションを提供します。FSMを使用することで、ゲーム開発者は、ゲームオブジェクト同士やゲーム世界との相互作用を定義するステートと遷移のシステムを作成することができます。

FSMは、有限個のステートとその間の遷移としてシステムを表現する数学的モデルです。ゲーム開発のコンテキストでは、FSM を使用して、プレイヤー、敵、NPC などのゲームオブジェクトの動作を表現できます。

FSM アドオンはHierarchical Finite State Machine(階層的有限ステートマシン:HFSM) アプローチを実装していませんが、それでも複雑な動作を作成する方法を提供します。FSMアドオンの各ステートは、1つ以上のサブステートマシン(子マシン)を含むことができ、より具体的な動作や条件を定義することができます。子マシンは並列に実行できるため、純粋なHFSMシステムの最も大きな欠点である、ステートの並列実行を解決できます。

ゲーム開発にFSMを使用することで、メンテナンスの容易さ、整理のしやすさ、複雑さの軽減など、いくつかの利点が得られます。FSMを使用することで、ゲーム開発者は、ゲームのステートとビヘイビアを管理するための、より効率的でスケーラブルなシステムを構築することができます。

overview

ダウンロード

バージョン リリース日 ダウンロード
2.0.3 Feb 09, 2024 Fusion FSM 2.0.3 Build 420

特徴

  • FSMアドオンは、コードのみの有限ステートマシンソリューションを提供
  • 同じゲームオブジェクト上で複数のステートマシンを並行して実行可能
  • 1つまたは複数のサブステートマシン(子マシン)を使用することで、ステートの整理が向上
  • 遷移ロジックは、ステートの優先順位とCanEnter/CanExitコールを使用するか、より伝統的なアプローチでステート間の遷移条件を定義することで処理可能

基本構造

FSMアドオンはコードのみのソリューションで、ステートと遷移ロジックはすべてコードで定義されます。アドオンの基本構造は以下の通りです:

StateMachineController
ゲームオブジェクトに追加する必要があるスクリプトで、そのオブジェクトに割り当てられている1つ以上のステートマシンの更新を担当します。

StateMachine ステートマシン
StateMachineはステートマシンを表すプレーンなクラスで、主なプロパティ(例: アクティブなステート、ステート時間)を格納するために使用されます。また、(TryActivateStateForceActivateState といったメソッドを使って)どのステートをアクティブにするかを制御することができます。

**状態
ステートは特定の動作を表し、ゲームオブジェクトの階層に追加される NetworkBehaviour (StateBehaviour から継承) か、プレーンなクラス (State から継承) のどちらかになります。

備考: ステートマシンの特定の機能に必要なプロパティを追加するために、共通の基本クラスを作成することを推奨します。例えば、共通の基本クラス EnemyAIStateEnemyAI コンポーネントへの参照を持つことができます。詳しくはステートの拡張のセクションを参照してください。

始め方

FSM アドオンの使用を開始するには、以下の手順に従ってください。

**1)**ネットワークオブジェクトに StateMachineController スクリプトを追加します。

overview

StateMachineControllerは、ネットワークオブジェクト階層内のすべてのステートマシンのネットワークデータの更新と同期を行います。

**ステートを表すステート・ビヘイビア・スクリプトを作成し、ネットワーク・オブジェ クト階層のオブジェクトに割り当てます。

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

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 - 実行中のパーティクルエフェクトやサウンドを停止する時などに使用

遷移ロジック

あるステートから別のステートへの遷移には2つの方法があります。

1. StateMachine の制御

StateMachineには、次のステートを設定するための基本的なメソッドがあります。

TryActivateState。 ステートマシンは現在のステートの CanExitState と次のステートの CanEnterState をチェックします。両方がパスすると、ステートが切り替わります。

注意: 現在のステートで "Check Priority On Exit" が有効になっている場合、新しいステートの優先度が現在のステートと同じか高い場合にのみ、ステートが切り替わります。

**TryDeactivateState***を呼び出すのと同じです。 これは TryActivateState(defaultState)` を呼び出すのと同じです。デフォルトステートとは、ステートマシンを作成するときにステート配列に渡される最初のステートのことです。

TryDeactivateState
これは TryActivateState(defaultState) を呼び出すのと同じです。デフォルトのステートとは、ステートマシンを作成するときにステート配列に渡される最初のステートのことです。

ForceActivateState/ForceDeactivateState
これらは TryActivateState/TryDeactivateState と似ていますが、 CanEnterState, CanExitState とステートの優先順位はチェックされません。

TryToggleState/ForceToggleState
これらのメソッドはステートのオンとオフを切り替えます。スイッチオフ(非アクティブ化)すると、デフォルトのステートがアクティブになります。

ステートの優先順位とEnter/Exit条件を正しく設定することで、ステートマシン全体を制御することができます。以下は、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呼び出し中にそのステートからのすべての遷移がチェックされます。遷移は、複数のステートで異なるステートへの同じ遷移ロジックを使用する場合や、複数の異なるステートマシンで同じ遷移ロジックを使用する場合に特に便利です。このように、遷移ロジックは1度だけ記述すれば、いくつのステートでも使用できます。

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 呼び出しで収集されます。

子マシンは、親ステートの OnEnterStateOnFixedUpdateOnExitState メソッド、または定義された遷移から制御する必要があります。

子マシンは親ステートがアクティブなときに更新されます。その他の点では、標準のステートマシンと同じです。子マシンのステートは、階層内の 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など)を呼び出したりすることは避けてください。
  • ビジュアルには常に Render メソッドを使用してください。OnEnterStateOnExitState はプロキシでは呼び出されないことに注意してください。
  • IsPaused プロパティを設定することで、ステートマシンの実行を一時停止することができます。
  • ステートマシーンの実行を一時停止するには、IsPaused プロパティを設定します。ステートにカスタム・データを直接格納することも可能ですが(下の章を参照)、このプロパティを設定する方がはるかに便利です。
  • 同じオブジェクトで複数のステートマシンを並行して実行することは珍しくありません。一般的に、複数のステートマシンを実行する理由は2つあります。
    • 複数のステートマシンのロジックを必要とするステート(例えば、AI用のマシンとアニメーションやビジュアル用のマシンなど)。
    • 例えば、攻撃、パトロール、サーチなどの動作を実行する一般的なAIマシンと、走る、斜行する、ジャンプパッドを使う、ギャップを飛び越えるなどの動作を実行する移動AIマシンなどです。

デバッグ

実行中に StateMachineController コンポーネントでゲームオブジェクトを選択すると、コンポーネントインスペクタにデバッグ情報が表示されます。

overview

ステートマシンの動作をデバッグするために、詳細なログが可能です。StateMachineControllerインスペクタでログを有効にすると、収集されたすべてのステートマシンがログに記録されます。より詳細なアプローチが必要な場合、ユーザーは特定のステートマシンのロギングをオンまたはオフにできます。

C#

_stateMachine.EnableLogging = true;
overview

カスタムステートデータ

上級ユーザー向けに、ステートを継承するときにカスタム・ネットワーク・データを定義するオプションが用意されています。こちらを実行するためには、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