PureFsmは、Unityで使える軽量なステートマシンライブラリです。
- PureC#で実装されているため、Pureなステートマシンが記述できます。
- UniTaskに対応しているため、非同期処理を簡単に記述できます。
- DIコンテナとの連携が可能です。各Stateやステートマシンに静的な依存性を注入することが出来ます。
Warning
UniTaskが必須なので、先にUniTaskをインストールしてください。
その後Unity Package Managerで以下のURLを追加してください。
https://github.com/qemel/PureFsm.git?path=/src/PureFsm
using PureFsm;
public class SampleFsm : Fsm<SampleFsm>
{
public SampleFsm(IEnumerable<IState<SampleFsm>> states) : base(states)
{
AddTransition<IdleState, WalkState>((int)EventId.Walk);
AddTransition<WalkState, IdleState>((int)EventId.Idle);
_ = Run<IdleState>();
}
}
public class IdleState : IState<SampleFsm>
{
public UniTask<int> EnterAsync(CancellationToken token)
{
Debug.Log("Enter IdleState");
return (int)EventId.Walk;
}
}
public class WalkState : IState<SampleFsm>
{
public async UniTask<int> EnterAsync(CancellationToken token)
{
await UniTask.Delay(1000, cancellationToken: token);
Debug.Log("Enter WalkState");
return (int)EventId.Idle;
}
}
public enum EventId
{
Walk,
Idle
}
Fsmを定義したいクラスにFsm<T>
を継承します。T
には自分自身を入れてください。
using PureFsm;
public class SampleFsm : Fsm<SampleFsm> // SampleFsm自身を指定
{
}
そのコンストラクタにて、対象となるステートを追加します。コンストラクタにはIEnumerable<IState<T>>
を渡してください。baseクラスのコンストラクタに渡すことで、ステートを追加できます。
public class SampleFsm : Fsm<SampleFsm>
{
public SampleFsm(IEnumerable<IState<SampleFsm>> states) : base(states) // ステートを追加するコンストラクタ
{
}
}
ステートとして、IState<T>
を実装したクラスを作成します。
EnterAsync()
にはステートに入った時の処理を記述します。戻り値にてステート遷移用のイベントIDを返します。
ExitAsync()
にはステートから出る時の処理を記述します(ExitAsync()
の実装は任意です)。
public interface IState<T> where T : Fsm<T>
{
UniTask<int> EnterAsync(CancellationToken token); // ステートに入った時の処理
UniTask ExitAsync(CancellationToken token) => UniTask.CompletedTask; // ステートから出た時の処理(任意)
}
ステートの実装例は以下の通りです。
public class IdleState : IState<SampleFsm>
{
public UniTask<int> EnterAsync(CancellationToken token)
{
Debug.Log("Enter IdleState");
return 1; // 例えばAddTransition<IdleState, WalkState>(1)と書かれていると、WalkStateに遷移する(後述)
}
public UniTask ExitAsync(CancellationToken token)
{
Debug.Log("Exit IdleState");
return UniTask.CompletedTask;
}
}
public class WalkState : IState<SampleFsm>
{
public async UniTask<int> EnterAsync(CancellationToken token)
{
await UniTask.Delay(1000, cancellationToken: token); // async/awaitで非同期処理を記述できる
Debug.Log("Enter WalkState");
return 0;
}
public async UniTask ExitAsync(CancellationToken token)
{
await UniTask.Delay(1000, cancellationToken: token);
Debug.Log("Exit WalkState");
return UniTask.CompletedTask;
}
}
最後に、コンストラクタ内でAddTransition<IState<T>, IState<T>>(int eventId)
を使って、ステート間の遷移を追加します(コンストラクタ以外で追加することも出来ます)。
Warning
eventId
には-1
を利用しないでください。これはステートマシンの終了を意味します(後述)。
public class SampleFsm : Fsm<SampleFsm>
{
public SampleFsm(IEnumerable<IState<SampleFsm>> states) : base(states)
{
AddTransition<IdleState, WalkState>(0); // IdleStateからWalkStateへの遷移に、イベントIDを0として追加
AddTransition<WalkState, IdleState>(1); // WalkStateからIdleStateへの遷移に、イベントIDを1として追加
}
}
Note
int
での管理がつらい場合は、enum
を使って管理することも出来ます(型レベルでは対応していません)。
public enum EventId
{
Walk,
Idle
}
public class SampleFsm : Fsm<SampleFsm>
{
public SampleFsm(IEnumerable<IState<SampleFsm>> states) : base(states)
{
AddTransition<IdleState, WalkState>((int)EventId.Walk);
AddTransition<WalkState, IdleState>((int)EventId.Idle);
}
}
以上で、ステートマシンの実装は完了です。
ステートマシンを実行するには、Fsm<T>.Run<T>()
メソッドを呼び出します。これは継承先のクラスのみが実行できるので、もし外部から実行したい場合は別途public
メソッドを用意してください。
public class SampleFsm : Fsm<SampleFsm>
{
public SampleFsm(IEnumerable<IState<SampleFsm>> states) : base(states)
{
AddTransition<IdleState, WalkState>(0);
AddTransition<WalkState, IdleState>(1);
_ = Run<IdleState>(); // IdleStateからステートマシンを開始
}
public void RunWalkState()
{
_ = Run<WalkState>(); // WalkStateからステートマシンを開始
}
}
ステートマシンを終了するには、Fsm<T>.Stop()
メソッドを呼び出します。
public class SampleFsm : Fsm<SampleFsm>
{
public SampleFsm(IEnumerable<IState<SampleFsm>> states) : base(states)
{
AddTransition<IdleState, WalkState>(0);
AddTransition<WalkState, IdleState>(1);
_ = Run<IdleState>();
}
public void StopFsm()
{
Stop(); // ステートマシンを終了
}
}
また、ステートにて、そのステートの移動先がない場合は、EnterAsync
メソッドの戻り値に-1
を返すことで、ステートマシンを終了させることができます。
public class EndState : IState<SampleFsm>
{
public UniTask<int> EnterAsync(CancellationToken token)
{
Debug.Log("Enter EndState");
return -1; // ステートマシンを終了
}
}
先ほどのFsm<T>.Run<T>()
をawait
すれば、ステートマシン自体の終了を待つことも可能です。
public class SampleFsm : Fsm<SampleFsm>
{
public SampleFsm(IEnumerable<IState<SampleFsm>> states) : base(states)
{
AddTransition<IdleState, WalkState>(0);
AddTransition<WalkState, IdleState>(1);
}
public async UniTask RunIdleAsync()
{
await Run<IdleState>(); // IdleStateからステートマシンを開始し、ステート自体の終了を待つ
Debug.Log("End");
}
}
PureFsmはDIコンテナとの連携が可能です。これによって各Stateに静的な依存性を注入した状態でステートマシンを構築することが出来ます。
以下のように、それぞれのStateにコンストラクタインジェクションを行うことができます。
using PureFsm;
public class SampleFsm : Fsm<SampleFsm>
{
public SampleFsm(IEnumerable<IState<SampleFsm>> states) : base(states)
{
// ...
}
}
public class IdleState : IState<SampleFsm>
{
private readonly Foo _foo;
public IdleState(Foo foo)
{
_foo = foo;
}
public UniTask<int> EnterAsync(CancellationToken token)
{
return 1;
}
}
public class WalkState : IState<SampleFsm>
{
private readonly Bar _bar;
private readonly Baz _baz;
public WalkState(Bar bar, Baz baz)
{
_bar = bar;
_baz = baz;
}
public UniTask<int> EnterAsync(CancellationToken token)
{
return 0;
}
}
これらはVContainerで以下のように解決できます。
using PureFsm;
using VContainer;
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<SampleFsm>(Lifetime.Singleton);
builder.Register<Foo>(Lifetime.Singleton).As<IState<TestFsm>>();
builder.Register<Bar>(Lifetime.Singleton).As<IState<TestFsm>>();
builder.Register<Baz>(Lifetime.Singleton).As<IState<TestFsm>>();
}
}
これによって、自動的にFsmのインスタンスと各Stateのインスタンスが解決されます。