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

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

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

 土屋がゲームプログラミングの基幹制御に使っていきたいと考えている、絶賛開発中のスタック型ステートマシン実装であるT2sFSMをまた改良したので紹介します。

前回の記事

 前回の記事はこちら。このステートマシンの機能についてはその1~その2も参照してください。

someiyoshino.info

T2sFSM(ツカサ式スタック型ステートマシン)

コードはこちら

//T2sFSM ver.4(20250223)
//Copyright (c) 2022-2024,2025 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 isSuspend;

        //無限ループ防止用デバッグカウンタ
        private int debugCounter;

        public void Update(TContext context)
        {
            //無限ループ検出用カウンタをリセットする
            debugCounter = 0;
            
            //スタックが空になるまで巡回する
            while (stateStack.TryPop(out IState state))
            {
                //ループ終了フラグを下ろす
                isSuspend = false;

                //ステートを実行する
                state.Update(context, this);

                //ステート内でHalt()が実行された場合、ループを抜ける
                if (isSuspend) break;

                //デバッグカウンタを+1し、規定回数を超えていた場合、
                //無限ループが発生している物としてエディタを一時停止する。
                if (debugCounter++ > 100000)
                {
#if UNITY_EDITOR
                    UnityEngine.Debug.Log("FSM Stopped");
                    UnityEngine.Debug.Break();
#endif
                    return;
                }
            }
        }

        //ステートをスタックする
        public void Push(IState state)
        {
            stateStack.Push(state);
        }

        //ステートの巡回を終了する。
        public void ReserveSuspend()
        {
            //ループ終了フラグを立てる
            isSuspend = true;
        }

        //ステートの巡回を終了する。
        public void ReserveSuspend(IState state)
        {
            //ステートをスタックする
            Push(state);
            //ループ終了フラグを立てる
            isSuspend = true;
        }

        public bool IsIdle()
        {
            //ステートスタックが空の場合、アイドル状態とする
            return stateStack.Count == 0;
        }
    }
}

解説(差分)

 以下、前回(ver3)からの改良箇所。

Push()でステートをまとめてスタック出来るようにしていたのをやめた。

 前回、Push()メソッドにStack<IState>を引数に渡せる多重定義verを追加したのですが、やっぱり無しにしました。

 前回の段階では、ステートマシンの外にキューイング待ちのステート群が滞留しているのが気持ち悪く感じたため、一気に流し込めるヘルパーメソッドとして用意したのですが、考えてみれば「パース前のシナリオスクリプト」というキューイング待ちの巨大なデータリソースが常に存在する事に気づき、なら流し込む必要無いなとなりました。

無限ループチェック用のカウンタのリミット値を1000から100000に拡張した

 実コードで検証を進めていたら、リミット値1000に簡単に到達してしまったため(つまり、あるステートマシンが1フレーム中に実行するステート数が1000に達する事が普通にあった)、ひとまず二桁増やしました。現在の検証範囲では、さすがにこれ以上は増える必要無い筈。

Halt()の名称をReserveSuspend()に変更した

 Halt()は、ステートマシンのplayerloopに相当する巡回処理を終了するメソッドで、ステート中から呼び出された場合は、そのステートの処理が終了した時点でループを抜けます。Halt()が実行された瞬間に処理が終了する訳では無いのですが、その事がHalt()という字面からは自明でなかったため、名称を変更しました。この名前が適切なのかどうかも自信がありませんが、ひとまずこれで。

 ちなみにHaltという名前は、ゲームが走査線割り込みを基準に描画処理をタイミング制御していた頃に由来する名称*1なんですが、このメソッド自体は描画処理タイミングとは直接関係ないので、その意味でも名前を変えたいと考えていました。

 また、ステートマシンの使用時、「現在のステートを再スタックした後Halt()する」という処理が非常に多かったので、これをまとめておこなう多重定義verも追加しました。

スタックしているスタートの有無を確認するIsIdle()を追加

 検証中、あるステートマシンが処理を実行中かどうか(つまり、スタックされているステートがあるかどうか)を知りたい事態が多発したので、その役目を担うIsIdle()メソッドを追加しました。

おわりに

 現在、ADV用のテキストレイヤフレームワークを実装していまして、T2sFSMを使い倒している最中です。今回の更新もその作業の成果と言えます。

 実際に使ってみると気付くことが多く、機能を増やしたり、増やした機能を削ったりを延々繰り返しています。今は、いわゆるステートを進行させるための「シグナル」を、T2sFSM自身の機能として内包すべきか検討しています。

宣伝「Unityシェーダープログラミングの教科書シリーズ」

 土屋は同人誌「Unityシェーダープログラミングの教科書」シリーズを書いています。現在以下の5冊が出ています。

 これらの同人誌は、現在BOOTHの下記ストアにてPDF版を有料頒布しています。

s-games.booth.pm

 Unityのシェーダープログラミング関連について、世界で最も詳細な本だと自負しています。リンク先にサンプルページがあるので、是非御覧になってください。

*1:土屋の場合はYaneuraoGameScript2000に由来