Finite State Machine
{% toc NoHeader %}
Overview
概要
FusionのFinite State Machine(有限状態マシン:FSM)アドオンは、ゲームの状態とビヘイビアを管理する強力なソリューションを提供します。FSMを使用することで、ゲーム開発者は、ゲームオブジェクト同士やゲーム世界との相互作用を定義する状態と遷移のシステムを作成することができます。
FSMは、有限個の状態とその間の遷移としてシステムを表現する数学的モデルです。ゲーム開発のコンテキストでは、FSM を使用して、プレイヤー、敵、NPC などのゲームオブジェクトの動作を表現できます。
FSM アドオンはHierarchical Finite State Machine(階層的有限状態マシン:HFSM) アプローチを実装していませんが、それでも複雑な動作を作成する方法を提供します。FSMアドオンの各状態は、1つ以上のサブ状態マシン(子マシン)を含むことができ、より具体的な動作や条件を定義することができます。子マシンは並列に実行できるため、純粋なHFSMシステムの最も大きな欠点である、状態の並列実行を解決できます。
ゲーム開発にFSMを使用することで、メンテナンスの容易さ、整理のしやすさ、複雑さの軽減など、いくつかの利点が得られます。FSMを使用することで、ゲーム開発者は、ゲームの状態とビヘイビアを管理するための、より効率的でスケーラブルなシステムを構築することができます。
ダウンロード
バージョン | リリース日 | ダウンロード | |
---|---|---|---|
2.0.4 | Aug 20, 2024 | Fusion FSM 2.0.4 Build 629 |
特徴
- FSMアドオンは、コードのみの有限状態マシンソリューションを提供
- 同じゲームオブジェクト上で複数の状態マシンを並行して実行可能
- 1つまたは複数のサブ状態マシン(子マシン)を使用することで、状態の整理が向上
- 遷移ロジックは、状態の優先順位とCanEnter/CanExitコールを使用するか、より伝統的なアプローチで状態間の遷移条件を定義することで処理可能
基本構造
FSMアドオンはコードのみのソリューションで、状態と遷移ロジックはすべてコードで定義されます。アドオンの基本構造は以下の通りです:
StateMachineController
ゲームオブジェクトに追加する必要があるスクリプトで、そのオブジェクトに割り当てられている1つ以上の状態マシンの更新を担当します。
StateMachine
状態マシン
StateMachine
は状態マシンを表すプレーンなクラスで、主なプロパティ(例: アクティブな状態、状態時間)を格納するために使用されます。また、(TryActivateState
や ForceActivateState
といったメソッドを使って)どの状態をアクティブにするかを制御することができます。
**状態
状態は特定の動作を表し、ゲームオブジェクトの階層に追加される NetworkBehaviour
(StateBehaviour
から継承) か、プレーンなクラス (State
から継承) のどちらかになります。
備考: 状態マシンの特定の機能に必要なプロパティを追加するために、共通の基本クラスを作成することを推奨します。例えば、共通の基本クラス EnemyAIState
は Enemy
と AI
コンポーネントへの参照を持つことができます。詳しくは状態の拡張のセクションを参照してください。
始め方
FSM アドオンの使用を開始するには、以下の手順に従ってください。
**1)**ネットワークオブジェクトに StateMachineController
スクリプトを追加します。
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...");
}
}
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)` を呼び出すのと同じです。デフォルト状態とは、状態マシンを作成するときに状態配列に渡される最初の状態のことです。
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
フラグが遷移に設定されている場合、関連する状態の 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
など)を呼び出したりすることは避けてください。 - ビジュアルには常に Render メソッドを使用してください。
OnEnterState
とOnExitState
はプロキシでは呼び出されないことに注意してください。 IsPaused
プロパティを設定することで、状態マシンの実行を一時停止することができます。- 状態マシーンの実行を一時停止するには、
IsPaused
プロパティを設定します。状態にカスタム・データを直接格納することも可能ですが(下の章を参照)、このプロパティを設定する方がはるかに便利です。 - 同じオブジェクトで複数の状態マシンを並行して実行することは珍しくありません。一般的に、複数の状態マシンを実行する理由は2つあります。
- 複数の状態マシンのロジックを必要とする状態(例えば、AI用のマシンとアニメーションやビジュアル用のマシンなど)。
- 例えば、攻撃、パトロール、サーチなどの動作を実行する一般的なAIマシンと、走る、斜行する、ジャンプパッドを使う、ギャップを飛び越えるなどの動作を実行する移動AIマシンなどです。
デバッグ
実行中に StateMachineController
コンポーネントでゲームオブジェクトを選択すると、コンポーネントインスペクタにデバッグ情報が表示されます。
状態マシンの動作をデバッグするために、詳細なログが可能です。StateMachineControllerインスペクタでログを有効にすると、収集されたすべての状態マシンがログに記録されます。より詳細なアプローチが必要な場合、ユーザーは特定の状態マシンのロギングをオンまたはオフにできます。
C#
_stateMachine.EnableLogging = true;
カスタム状態データ
上級ユーザー向けに、状態
を継承するときにカスタム・ネットワーク・データを定義するオプションが用意されています。こちらを実行するためには、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
を継承している) への参照を状態に保存し、必要なネットワークデータをそこに保存するだけでも全く問題ありません (状態の拡張 セクションのコードスニペットを参照してください)。