Unity用の激薄ステートマシンライブラリを作りました
はじめに
Unity用のステートマシンライブラリを作りました。名前はPureFsmです。
PureFsm
PureFsmはUnity用のステートマシンライブラリです。
特徴
- PureC#で実装できる
- UniTask必須
- その分EnterとかExitとかをawaitでき、内部に非同期処理を書きやすい
- DIフレンドリーで、ステートマシンや各ステートを簡単にDIできる
- VContainerの使用を推奨します
- 激薄
- 200行くらいのコード
使い方
上記リンクのREADMEに書いているので、コード例だけ載せます。
ステートマシンの作成
using PureFsm;
public class SampleFsm : Fsm<SampleFsm>
{
public SampleFsm(IEnumerable<IState<SampleFsm>> states) : base(states)
{
AddTransition<IdleState, WalkState>((int)Transition.Walk);
AddTransition<WalkState, IdleState>((int)Transition.Idle);
_ = Run<IdleState>();
}
}
public enum Transition
{
Idle,
Walk,
}
ステートの作成
public class IdleState : IState<SampleFsm>
{
readonly Foo _foo; // 各ステートにDIできる
public IdleState(Foo foo)
{
_foo = foo;
}
public async UniTask<int> EnterAsync(CancellationToken token)
{
await UniTask.Delay(1000, cancellationToken: token);
await _foo.DoSomethingAsync();
return (int)Transition.Walk;
}
public async UniTask ExitAsync(CancellationToken token)
{
Debug.Log("Exit IdleState");
}
}
public class WalkState : IState<SampleFsm>
{
public async UniTask<int> EnterAsync(CancellationToken token)
{
Debug.Log("Enter WalkState");
while (!token.IsCancellationRequested) // Update()の代わり
{
await UniTask.Yield();
if (Input.GetKeyDown(KeyCode.Space))
{
return (int)Transition.Idle;
}
}
}
public async UniTask ExitAsync(CancellationToken token)
{
await UniTask.Delay(1000, cancellationToken: token);
Debug.Log("Exit WalkState");
}
}
Update()
に相当するものはないので、EnterAsync()
の中でwhile
ループを使う感じになります。
DI(VContainer)
using PureFsm;
using VContainer;
public class GameLifetimeScope : LifetimeScope
{
protected override void Configure(IContainerBuilder builder)
{
builder.Register<SampleFsm>(Lifetime.Singleton);
builder.Register<IdleState>(Lifetime.Singleton).As<IState<SampleFsm>>();
builder.Register<WalkState>(Lifetime.Singleton).As<IState<SampleFsm>>();
builder.Register<Foo>(Lifetime.Singleton);
}
}
作った動機・気を付けたこと
あんまり大袈裟なステートマシンを使う気が無かった
Unityのステートマシンライブラリはいくつかあるのですが、あんまり大袈裟なものを使う気が無かったので、最小限のもので作ってみました。
クラスが入れ子になるタイプのものをあまり使いたくなかった
ステートマシンの中にクラスを定義してステートを表現するタイプのものは、ファイルを分けづらい他、ステートマシン自体の依存クラスが多くなりがちだったので、そこを分けたいという気持ちがありました。
できるだけpublicな部分を少なくしたかった
個人的に、ステートマシンはほとんどエントリポイントとして機能して、外部から触れる部分はなるべく少なくするべきと思っていたので、そのようにしてみました。
現在のステートも公開していないため、基本的にはステート内部でやりたい処理は全部書いておこうというスタンスです。
DIしやすいものが欲しかった
コンストラクタで依存性が解決できると非常に便利なので、そういう方針で作りました。
最初はステート遷移に相当するものまでコンストラクタ注入しようと思っていたのですが、そこそこ登録が面倒になりそうでやめました。
override
を使いたくなかった
ほんとうに個人の好みですが、public override void Enter()
の記述量があまり好きではなく、出来ればIState
を実装するだけで済ませたかったので、そうしました。
できるだけ記述量を減らしたかった
最初は状態の遷移をenum
型で管理して厳密にいこうと思っていたのですが、ジェネリックの見た目がきつめになったので、記述量を減らすべくint
型で管理することにしました。
とはいえ普通にenum
型をつくり、それを適宜int
にキャストすればいいだけなのでそこまで使用感に問題は無いと捉えています。
遷移自体をちゃんとした型として扱うことに対してそこまでのメリットを感じなかったのも、このような作り方にした理由の一つです。
ただ、ステートマシン自体をloggerに出すときなどはint
型のままだと分かりづらいなーという悩みが少々あります。
おわりに
バグ報告などあれば是非お願いします...!
Discussion