ステートマシンの実装は色々ありますが、asyncを用いたシンプルなものを紹介します。
と言いつつも実際はasyncを使う必要もなくて、単純にステート遷移時に実行する関数をasyncにしてるだけです。
ただ、世に出回っているステートマシンがOnEnter,OnUpdate,OnExitみたいな実装が多いので、asyncを使うならOnEnterだけで十分だろうという思想です。
使い方
記事の下の方にステートマシンの実装は載せるので、先に使用例を。
private enum GameState { Morning, Night, Result } private QStateMachine<GameState> stateMachine = null; private void Awake() { // StateMachineを作成 this.stateMachine = new QStateMachine<GameState>().AddTo(this); // 各State遷移時に実行する関数を登録 this.stateMachine.SetStateAction(GameState.Morning, MorningAsync); this.stateMachine.SetStateAction(GameState.Night, NightAsync); this.stateMachine.SetStateAction(GameState.Result, ResultAsync); // Stateの遷移を登録(ここで登録した遷移のみ出来る。この制約がある方が便利な場合が多い) this.stateMachine.SetTransition(GameState.Morning, GameState.Night); this.stateMachine.SetTransition(GameState.Morning, GameState.Result); this.stateMachine.SetTransition(GameState.Night, GameState.Morning); this.stateMachine.SetTransition(GameState.Night, GameState.Result); // StateMachineを開始 this.stateMachine.Start(GameState.Morning); // Resultステートへの遷移チェック ResultCheck(); } // ステートマシンとは別の場所でリザルトに遷移するチェックを回せる private async void ResultCheck() { // 30秒後にResultステートに遷移 await UniTask.WaitForSeconds(30); } // Morningステート遷移時に実行される private async void MorningAsync(CancellationToken ct) { try{ // 10秒後にNightステートに移行 await UniTask.WaitForSeconds(10, cancellationToken: ct); TrySetState(GameState.Night); } catch (OperationCanceledException) { // この関数実行中に外部によってステート遷移した場合の処理 } } // Nightステート遷移時に実行される private async void NightAsync(CancellationToken ct) { // 5秒後にMorningステートに移行 await UniTask.WaitForSeconds(5, cancellationToken: ct); TrySetState(GameState.Morning); } // Resultステート遷移時に実行される private void ResultAsync(CancellationToken ct) { // asyncじゃなくてもok Debug.Log("Result"); }
IDisposableの実装になっているので、AddToやDisposeをしてもらえると。
また、SetTransitionには任意のconditionを引数に与えられ、遷移に条件を付与できます。
TrySetStateになっているのは、ステート遷移が登録されていなかったりconditionを満たさない場合にfalseとするためです。
ソースコード
R3を使っていますが、必要に応じてUniRxや通常のTask等にしてもらえれば。
(そんなに変える部分は無いと思います。)
using System; using System.Collections.Generic; using System.Threading; using R3; namespace QuestBase { public class QStateMachine<T> : IDisposable where T : Enum { private Dictionary<T, List<(T state, Func<bool> condition)>> transitions = new Dictionary<T, List<(T, Func<bool>)>>(); private Dictionary<T, Action<CancellationToken>> stateActions = new Dictionary<T, Action<CancellationToken>>(); private T currentState; private CancellationTokenSource stateCts; private Subject<T> onStateChanged = new Subject<T>(); public T CurrentState => this.currentState; public Observable<T> OnStateChanged => this.onStateChanged; public void Dispose() { this.onStateChanged.Dispose(); AsyncUtil.CancelAndDisposeCTS(ref stateCts); } public void Start(T startState) { SetState(startState); } public void SetTransition(T from, T to, Func<bool> condition = null) { if (this.transitions.TryGetValue(from, out var list)) { list.Add((to, condition)); } else { this.transitions[from] = new List<(T, Func<bool>)> { (to, condition) }; } } public void SetStateAction(T state, Action<CancellationToken> action) { this.stateActions[state] = action; } public bool TrySetState(T state) { if (this.transitions.TryGetValue(this.currentState, out var stateList)) { foreach (var toState in stateList) { if (toState.state.Equals(state) && (toState.condition?.Invoke() ?? true)) { SetState(state); return true; } } } return false; } private void SetState(T state) { this.currentState = state; if (this.stateActions.TryGetValue(this.currentState, out var func)) { AsyncUtil.CancelAndCreateCTS(ref stateCts); func.Invoke(this.stateCts.Token); } else { AsyncUtil.CancelAndDisposeCTS(ref stateCts); } this.onStateChanged.OnNext(state); } } }