本ページにはプロモーションが含まれます
PlaymakerUnity

【Unity】Playmakerでベンチマークしてみたばい!

この記事は約9分で読めます。

はじめに

どうも、こんにちは。Playmaker大好きっ子です。今回は Unity + Playmakerで「状態遷移処理(FSM:Finite State Machine)」の処理速度をベンチマークした話ばい。博多弁は書いていた時のノリ…。

目的

なんか面白い過去記事みつけて、自分もPlaymakerのState動的生成をやりたくなりました。

https://qiita.com/kotauchisunsun/items/aab99b5f3b0239667911

 
Playmakerを多用してるので、状態遷移が多くなったときのパフォーマンスも少し気になり
「100ステートくらいなら大丈夫だけど、1000超えたらどうかな?」
──ってことで、3000ステートのFSMを自動生成して、処理速度をざっくりと簡単に測定してみました。
※Playmakerの自体はあくまで状態遷移をするためのツールであり、使うことで処理が速くなるものではないのです。

1FSMで3000状態遷移って……C# や PHP などの一般的なコードベースでこれをやったら、いわゆる「GODクラス」や「スパゲッティコード」そのもの
先輩シニアエンジニアに電源コード抜かれちゃいますね。
あくまでこれは簡単なPlaymakerのステートグラフの限界検証。ツールの挙動とボトルネックを知るための耐久テストっぽい何かと受け止めてほしいところ。期待しているような処理をどのくらいのベンチマークではないです

方法

以下のような構成で FSM を作成しました。あくまでUnity上のPlaymakerのグラフビューの描画などの処理時間を含めたものであることを認識しておいてほしい。実際フルC#がコンパイルされて実機で動く参考にはならないですよ。お遊びだよ。

  • 状態数:3000個
  • 各ステートでログ出力(DebugLog)
  • 最初のステートで開始時刻を取得
  • 最後のステートで終了時刻を取得して差分を計算
  • 処理時間を DebugFloat で出力

コード内で GetTimeInfo(RealTimeSinceStartup)を使って、正確な実行時間を測定しました。State0~State2999までの経過時間見れる感じです。

#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using HutongGames.PlayMaker;
using HutongGames.PlayMaker.Actions;
using System.Collections.Generic;

public class PlaymakerFsmGenerator
{
    [MenuItem("Tools/Generate FSM Benchmark with In-FSM Timer")]
    public static void GenerateFsmBenchmarkWithTimer()
    {
        GameObject go = new GameObject("GeneratedFsmObject");
        var fsmComponent = go.AddComponent<PlayMakerFSM>();
        var fsm = fsmComponent.Fsm;
        fsm.Name = "GeneratedFSM";

        var finishedEvent = FsmEvent.FindEvent("FINISHED");
        fsm.Events = new[] { finishedEvent };

        int totalStates = 3000;
        int columnCount = 50;
        float xOffset = 460f;
        float yOffset = 160f;

        var benchTimer = new FsmFloat("bench_timer");
        var currentTime = new FsmFloat("current_time");
        var elapsedTime = new FsmFloat("elapsed_time");
        fsm.Variables.FloatVariables = new[] { benchTimer, currentTime, elapsedTime };

        var states = new List<FsmState>();

        for (int i = 0; i < totalStates; i++)
        {
            var state = new FsmState(fsm)
            {
                Name = $"State_{i}",
                Position = new Rect((i % columnCount) * xOffset, (i / columnCount) * yOffset, 300, 100)
            };

            var actions = new List<FsmStateAction>();

            if (i == 0)
            {
                actions.Add(new DebugLog
                {
                    text = new FsmString { Value = "=== FSM開始 ===" },
                    sendToUnityLog = true,
                    logLevel = LogLevel.Info
                });

                actions.Add(new GetTimeInfo
                {
                    getInfo = GetTimeInfo.TimeInfo.RealTimeSinceStartup,
                    storeValue = benchTimer
                });
            }

            actions.Add(new DebugLog
            {
                text = new FsmString { Value = $"State {i} 通過中" },
                sendToUnityLog = true,
                logLevel = LogLevel.Info
            });

            if (i == totalStates - 1)
            {
                actions.Add(new GetTimeInfo
                {
                    getInfo = GetTimeInfo.TimeInfo.RealTimeSinceStartup,
                    storeValue = currentTime
                });

                actions.Add(new FloatOperator
                {
                    float1 = currentTime,
                    float2 = benchTimer,
                    operation = FloatOperator.Operation.Subtract,
                    storeResult = elapsedTime
                });

                actions.Add(new DebugLog
                {
                    text = new FsmString { Value = "=== FSM終了 ===" },
                    sendToUnityLog = true,
                    logLevel = LogLevel.Info
                });

                actions.Add(new DebugFloat
                {
                    floatVariable = elapsedTime,
                    sendToUnityLog = true
                });
            }

            state.Actions = actions.ToArray();
            state.SaveActions();

            if (i < totalStates - 1)
            {
                state.Transitions = new[]
                {
                    new FsmTransition
                    {
                        FsmEvent = finishedEvent,
                        ToState = $"State_{i + 1}"
                    }
                };
            }

            states.Add(state);
        }

        fsm.States = states.ToArray();
        fsm.StartState = "State_0";

        EditorUtility.SetDirty(fsmComponent);
        Debug.Log(" FSM(3000ステート)+ FSM内での経過時間記録が完了");
    }
}
#endif

Editorにこのスクリプトを配置。ToolsメニューにGenerate FSM Benchmark with In-FSM Timeがあるので実行するとFSMとステートがtotalStates分作成される。
C#でPlaymakerのアクションを動的に実装できるの面白いよね。今回この実験がメインだったり。

検証

3000ステートをTools/Generate FSM Benchmark with In-FSM Timerから実行して自動作成する。
最初のステートで時間取得、変数代入。以降のステートでDebugLog。最後のステートで経過時間差分計算。
ベンチはステート実行→FINISHED→次のステートが0~2999まで遷移する経過時間。
3000ステートで0.6秒。まぁ、許容範囲では。以前デリゲートでFSM実装したようなフルC#なら0.1秒未満くらいかな。


問題はPlaymakerのステートエディタの描画がもっさり。でも使えないレベルではない。アクションは挿入できるし、変数も作れる。

MiniMapがグレーで全部埋まるw フリーズしないですね、グラフビューはもっさりしてるけど、使えます。

結論

もう一度書いておく、あくまでUnity上のPlaymakerのグラフビューの描画などの処理時間を含めたものであることを認識しておいてほしい。

Playmakerはできる子。クリッカー系スマホゲームとか、VRとか、PCゲームつか実装してて、単体FSMの処理で3000ステートなんて絶対書かないもん。複数のFSMで実装するし、1FSMでもの凄く多くても100ステート使わないのでは?

100ステート遷移でのベンチ。0.02秒。十分ですよ。

10000ステート遷移でのベンチ。2秒。

グラフビューは10000ステートでももっさりだが、フリーズしないで使えた。

 

おわりに

Editor上でPlaymakerの1FSMあたりで扱うステートの数の簡単な目安にはなればと思いました。
Releaseビルドは全然速いですよ。前にも書きましたが、Playmakerはあくまで状態遷移をするためのツールであり、使うことで処理が速くなるものではないのです。