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

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

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

 以前紹介したステートマシンの少しだけ改良したので、その紹介記事になります。以前の記事はこちら。

someiyoshino.info

someiyoshino.info

前口上

 ご存知の方も多いでしょうが、土屋はライフワーク的にテキスト描画(というかADV)の汎用エンジンを作っています。時間がある時しか進められず、また、ここ最近は毎年一から作り直しては「今回のコンセプトもダメだったか……」と諦める事を繰り返してまして、なかなか完成には至っていません。

 今年は仕事が忙しくて趣味でUnityを触る事が出来なかったのですが、年の瀬が近いという事もありまして(?)、ひさびさに作業を再開しました。調べてみたら去年はUEFN&Verseメインだったためエンジン開発はお休みしていました。なので2年ぶりという事になります。

 あと今更ですが、UnityとUEFNの記事を同じブログに統合したのは失敗だったな……。今からでもまた分離しようかな……。

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

 そんなわけで現在久し振りにエンジン実装を進めてまして、その過程でT2sFSMを微修正しました(折角なのでコメントも厚めに書いておきました)。

//T2sFSM ver.3(20241108)
//Copyright (c) 2022-2023,2024 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 IsExit;

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

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

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

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

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

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

        //ステートの配列(順列)をスタックする
        public void Push(IState[] states)
        {
            for (var i = states.Length - 1; i >= 0; i--)
            {
                stateStack.Push(states[i]);
            }

        }
        
        //ループを抜ける(=ステートマシンの処理を終了して次フレームに送る)
        public void Halt()
        {
            //ループ終了フラグを立てる
            IsExit = true;
        }
    }
}

 「スタック型ステートマシン」というのは、その名の通り内部に状態をスタックする機能を持つ状態遷移機械の事です。ゲームで扱うようなオブジェクトの状態管理では、有限状態オートマトンよりも、スタック式*1の方が向いていると考えられています。詳しくは過去記事を参照してください。

 全体ロジックは過去記事で説明しているので、差分だけ触れておきます。

スタック巡回ループを抜ける時だけメソッドを実行するようにした。

 以前の実装では、各ステート内でステートマシンのループ巡回を「継続する時」だけEnableContinueLoop()を実行する形にしていました。ループの継続は無限ループに陥る可能性があるので、その責任をコーダーに明示させようとした為です。

 しかし、実際のステート処理ではほとんどの場合にループ継続が必要になり、ループを中断する処理は、突き詰めるとウェイト処理のみで発生すれば良いという事がわかりました。その為、ループ巡回を「中断する時」だけHalt()を実行する形に変更しました。ほぼすべてのステートで記述していたEnableContinueLoop()が必要なくなるので助かります。

 一方、当初懸念していた無限ループに陥る危険性は増えたわけですが、無限ループ防止用のデバッグカウンタがあるので、最悪な事態は避けられるだろうと考えています。

 余談ですが、"Halt"は「中断」の意味の英語で、8bitパソコン時代に割り込み処理を表す用語として頻出していました。YaneuraoGameScript2000とかでお馴染みですね(誰が知ってるんだ)。

PushState()をPush()にリネームした

 小さい話ですが、ステートをスタックする時に"xxx.PushState(~"がずらずら並ぶのがうっとうしかったので短くしました。ステート以外をプッシュする事無いので、これでも分かるだろうという事で。

ステートの配列をスタックするメソッドを追加した

 ステートの配列を受け取るPush()メソッドを多重定義しました。スタックはFIFOなので、連続するステートをPush()する場合、ステート群を逆順に登録する必要があります。これが頻出するとミスが出るしなにより面倒くさいので汎用メソッドにしました。ただ、本当にこれが必要なのかどうかまだ判断が付いてません。

2024/11/17追記

 このメソッドは、ファイルからシナリオを1行ずつ読み込み、あるタイミング(例えば改ページ)で読み込みを打ち切ってステートマシンにまとめて送信する事を想定した物でした。

 しかし、実装を進める過程で、このようなケースの場合、まずシナリオをローカルのStackに(つまり逆順に)格納し、それをステートマシンにFIFOでスタックし直す方がロジックを組みやいと分かり、このメソッドも引数にStackを取るように変更しました。

        //ステートのスタックをスタックする(つまり、逆順に格納される)
        public void Push(Stack<IState> states)
        {
            foreach (IState state in states)
            {
                stateStack.Push(state);
            }
        }

 配列版の場合、引数に入ってくるステート群が順列なのか逆順なのかが自明になりませんが、Stackであれば自明なので(?)コードが自己言及的になって良い感じです。スタック式ステートマシンの運用スタイルとしても自然かなと思います。

おわりに

 ゲームオブジェクトのように複雑な状態を管理する場合、スタック式ステートマシンは有効な手段です。ただ、知名度が低いのかあまり使っている所を見た事がなく、ノウハウも集積されていない印象があります。

 T2sFSMに興味が出ましたら是非使ってみてくださいませ。フィードバックを歓迎します(コメント他でどうぞ)。

 作りたいのはステートマシンではなくテキストレイヤ(雑に言えばTextMeshProの再実装)なので、頑張ります。

参考リンク(過去記事からの再掲)

JRPGの作り方:ゲーム開発者向けの手引き(2013)

How to Build a JRPG: A Primer for Game Developers code.tutsplus.com

スタック型ステートマシンがゲームアーキテクチャと相性が良いという話がされています。著者のDan Schuller氏は様々なゲーム会社を経て、現在はUnity社のエンジニアをされているようです(2022年当時)。

天才プログラマー養成ギプス(やねうらお)

「第八章.シーン管理クラスの設計2」 bm98.yaneu.com

「有限状態オートマトンは階層構造を表現するのに適していない」と解説している記事。やねうらおさんの著作「Windowsプロフェッショナルゲームプログラミング」の345ページにも「FSA(有限オートマトン)は、階層構造を表現するのに適していない」とあります。

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

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

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

s-games.booth.pm

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

*1:正確にはプッシュダウン・オートマトン