This document is about: FUSION 1
SWITCH TO

このページは編集中です。更新が保留になっている可能性があります。

有限ステート機械 (FSM)

Level 4

概要

Fusion用の有限ステートマシン (FSM) アドオンは、ゲームステートと挙動を管理する強力なソリューションを提供しています。FSMを使用するとゲームデベロッパーは、ゲームオブジェクトがお互いやゲ無ワールドとやり取りするステートとトランジションのシステムを作成できます。

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

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

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

overview

ダウンロード

バージョン リリース日 ダウンロード
1.2.0 Jul 11, 2023 Fusion FSM 1.2.0 Build 210

特徴

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

基本構造

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

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

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

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

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

開始方法

以下の手順に従って、FSM アドオンを使用してください。

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

overview

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...");
    }
}
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)` を呼び出すのと同じです。デフォルトのステートは、ステートマシンを作成するときにstates配列に渡される最初のステートです。

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

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

ステートマシン全体は、ステートの優先順位と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 ベースクラスを作成することを推奨しています。例えば、一般的な基底クラス PlayerStateBehaviour には AnimancerComponentCharacterController コンポーネントへの参照があります。全てのプレイヤーのステートビヘイビアは 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...");
        }
    }
}

ベストプラクティス

  • ネットワークステートの調整またはステート機械コントロールメソッド(TryActivateStateなど)をレンダリングメソッドから回避する
  • ビジュアルには必ずRenderメソッドを使用すること。OnEnterStateおよび OnExitStateはプロキシ上では呼び出されない点に注意してください。
  • ステート機械実行はIsPausedプロパティ設定によって保留可能
  • Stateプレーンクラスを使用する場合は、レファレンスをそのクラスで親のStateBehaviourに 保管して必要なネットワークデータを保管することを考慮すること。ステートで直接カスタムデータを保管することは可能ですが(次の章を参照のこと)、全く利便性はない。
  • It is not uncommon to have multiple state machines running in parallel on the same object. There are generally two reasons for running multiple state machines:
    • Multiple areas require state machine logic, such as one machine for AI and another for animations and visuals.
    • Certain logic benefits from parallel execution, such as a general AI machine for executing behaviors like attack, patrol, search, and a movement AI machine for executing movement behaviors like running, strafing, using jumppads, jumping over gaps, etc.

デバッギング

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

overview

ステート機械の挙動をデバッグするため、詳細なログが可能です。ログはStateMachineControllerインスペクタで有効にすることができます。ここには改修したすべてのステート機械をログします。より精巧なアプローチが必要な場合は、特定のステート機械についてログのオン・オフ切り替えを行うことができます。

C#

_stateMachine.EnableLogging = true;
overview

カスタムステートデータ

アドバンスユーザーには、Stateから継承するカスタムネットワークデータを定義するオプションがあります。これを行うには、GetNetworkDataWordCountWriteNetworkDataReadNetworkDataメソッドをオーバーライドする必要があります。

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