土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#unity ツカサ式スタック型ステートマシンT2sFSM(その2)

 前回、ツカサ式ステートマシンT2sFSMを紹介しました。
someiyoshino.info
 前回の実装はテキスト描画フレームワークの設計中に作った物だったのですが、その後の実装過程で、より分かりやすくかつ安全に使えるようにちょこちょこ改修を続けています。今回は主な改修部分について3点紹介。前回の記事と比較すると改修点が分かりやすいかもしれません。

ソースコード

 最新版のコードはGitHubにもあります。
github.com

//T2sFSM
//Copyright (c) 2022 tsukasa TSUCHIYA(T2/t_tutiya)
//This software is released under the MIT License.
//http://opensource.org/licenses/mit-license.php

using System.Collections.Generic;

namespace info.someiyoshino.Tsukasa
{
    //ツカサ式スタック型ステートマシン(Tsukasa system of Stack-Based FSM)
    public class T2sFSM<TContext>
    {
        public interface IState
        {
            public void Update(TContext context, T2sFSM<TContext> StateMachine);
        }

        private readonly Stack<IState> stateStack = new();

        private bool exitLoop;

        private int debugCounter;

        public void Update(TContext context)
        {
            debugCounter = 0;
            while (stateStack.TryPop(out IState result))
            {
                exitLoop = true;

                result.Update(context, this);

                if (exitLoop) break;

                if (debugCounter++ > 100)
                {
#if UNITY_EDITOR
                    UnityEngine.Debug.Break();
#endif
                    return;
                }
            }
        }

        public void PushState(IState state)
        {
            stateStack.Push(state);
        }

        public void EnableContinueLoop()
        {
            exitLoop = false;
        }
    }
}

改修①:Contextを経由せずにステートマシンにアクセス出来るようにした。

 IState.Update()の引数でT2sFSMオブジェクトを受け取り、Contextを経由せずにステートマシンにアクセス出来るようにしました。PushState()や後述するEnableContinueLoop()が若干書きやすくなります。
 引数でT2sFSMを渡すのが本当に必要なのか、前回の段階では判断できなかったのですが、ユースケースを考えていて「クラスXの外にあるステートマシンがXを駆動する場合」があることに気づき、必須であると判断しました。

改修②:ループを抜けない場合、それを明示するロジックに変えた。

 前回は、IState.Update()の戻り値(bool型)で、ステートの実行を続ける(false)のかステートマシン処理を終了するのか(true)を返していました。
 これ自体は機能するのですが、コードを書いていて頻繁に「ステートの実行を続けるのはtrueだったかfalseだったか……」と悩む羽目になってしまいました。構造上、戻り値を間違えると簡単に無限ループに陥るため、地味にストレスにもなっていました。
 そこで、T2sFSMM<>.EnableContinueLoop()メソッドを追加し、ステートマシンのループ処理を継続する際にはこのメソッドを呼ぶ物としました。無限ループに陥る危険がある処理は、コーダーがそれを明示する形式にしたわけです。IState.Update()の戻り値はvoidになり、考える事が減りました。

改修③:【暫定措置】デバッグ用の無限ループ検出カウンターを用意した。

 改修②でも書きましたが、T2sFSMはその構造上、実装をミスすると簡単に無限ループに陥ってしまうのが困りものです。
 これについての対処方法はまだ模索中なのですが、ひとまずループ回数をカウントして100回続いたら「まあ多分無限ループしているだろう」とみなし、UnityEditorを一時停止するようにしました(この状態で再生ボタンをクリックすると安全に処理を終了出来る)。
 デバッグ用の処理なのでリリース時にはサクりたいんですが、この処理自体が暫定なのでその辺は未対応です("UnityEngine.Debug.Break();"が処理系依存だし)。これのおかげでひとまず心理的な負担が随分緩和しました。