有限ステート機械 (FSM)
概要
Fusion用の有限ステートマシン (FSM) アドオンは、ゲームステートと挙動を管理する強力なソリューションを提供しています。FSMを使用するとゲームデベロッパーは、ゲームオブジェクトがお互いやゲ無ワールドとやり取りするステートとトランジションのシステムを作成できます。
FSMはシステムを有限数のステートとその間の遷移として表現する数理モデルです。ゲーム開発の文脈では、FSMはプレイヤー、敵、NPCなどのゲームオブジェクトの動作を表現するために使用できます。
FSMアドオンは階層有限ステートマシン(HFSM)アプローチを実装していませんが、複雑な動作を作成する方法を提供しています。FSMアドオンの各ステートは、1つ以上のサブステートマシン(子マシン)を含むことができ、より具体的な動作や条件を定義することができます。子マシンは並列に実行できるため、純粋なHFSMシステムの最も大きな欠点である、ステートの並列実行を解決できます。
ゲーム開発にFSMを使用することで、メンテナンスの容易さ、整理のしやすさ、複雑さの軽減など、いくつかの利点が得られます。FSMを使用することで、ゲーム開発者は、ゲームのステートとビヘイビアを管理するための、より効率的でスケーラブルなシステムを構築することができます。
ダウンロード
バージョン | リリース日 | ダウンロード | |
---|---|---|---|
1.2.0 | Jul 11, 2023 | Fusion FSM 1.2.0 Build 210 |
特徴
- FSMアドオンで、コードのみの有限ステートマシンソリューション実現。
- 同じゲームオブジェクト上で複数のステートマシンを並行して実行可能。
- 1つまたは複数のサブステートマシン(子マシン)を使用することで、ステートをさらに整理。
- トランジションロジックは、ステートの優先順位とCanEnter/CanExitコールを使用するか、従来のステート間の遷移条件を定義するアプローチで処理可能。
基本構造
FSMアドオンはコードのみのソリューションで、ステートとトランジションロジックはすべてコードで定義されます。アドオンの基本構造は以下の通りです:
StateMachineController`
ゲームオブジェクトに追加する必要があるスクリプトで、そのオブジェクトに割り当てられている1つ以上のステートマシンの更新を担当します。
**StateMachine**
StateMachineはステートマシンを表すプレーンなクラスで、主なプロパティ(例: アクティブなステート、ステート時間)を格納するために使用され、(
TryActivateStateや
ForceActivateState`などのメソッド経由)どのステートをアクティブにするかを制御するために使用することができます。
ステート
ステートは特定のビヘイビアを表し、ゲームオブジェクトの階層に追加される NetworkBehaviour
(StateBehaviour
から継承) か、プレーンクラス (State
から継承) のどちらかになります。
注意: ステートマシンの特定の機能に必要なプロパティを追加するために、共通の基底クラスを作成することをお勧めします。例えば、共通の基底クラス EnemyAIState
は Enemy
と AI
コンポーネントへの参照を持つことができます。詳しくはステートの拡張のセクションを参照してください。
開始方法
以下の手順に従って、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) ステートマシンを保持する任意の 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` - 実行中のパーティクルエフェクトやサウンドを停止する時などに使用します。
トランジションロジック
ステート間の遷移には、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
フラグがトランジションにセットされている場合、関連するステートの 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...");
}
}
}
ベストプラクティス
- ネットワークステートの調整またはステート機械コントロールメソッド(
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
コンポーネントでゲームオブジェクトを選択する際に、デバッグ情報がコンポーネントインスペクターに表示されます。
ステート機械の挙動をデバッグするため、詳細なログが可能です。ログは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
からの継承)をステートに保管し、そこで必要なネットワークデータを保管するだけで大丈夫です(コードスニペットは拡張ステートで確認してください)。