記事一覧へ戻る

Cluewren による自動プレイで Unity の性能回帰を測定する

ビルドごとに手動で同じルートをプレイして性能を測るのは手間がかかり、ビルド間の正確な比較は難しくなります。 オープンソース(Apache-2.0)の Cluewren では、開発者自身のコーディングエージェント(Claude Code または Codex)が Cluewren の MCP サーバー経由で Unity、Godot、Unreal Engine のエディタで開いたゲームをプレイします。 エージェントがコードを読み取って C# のハーネスを生成し、TypeSafe の高速な意思決定モデルである Jev がエンジンプラグインを介してプレイを実行し、エージェントが測定した根拠とともに判定を報告します。 このプレイ中に Framedash Unity SDK でフレームタイムを記録します。 二つのビルドで同じルートを走行し、どちらのビルドの、レベルのどの場所が遅くなったかを特定します。 候補ビルドの性能低下は、重いアセットやエフェクトの代用としてシミュレーションで発生させたものです。

使用した環境と前提条件

今回の検証には次の環境を使用しました。

  • Unity: Unity 6000.6.7f1
  • サンプルプロジェクト: Unity の FPS Microgame(MainScene)に Cluewren Unity プラグインを導入したプロジェクトのコピー
  • Cluewren の動作要件: Windows 11(テスト済み)、.NET 10 SDK、ソースからのビルド、TypeSafe API キー、Claude Code または Codex

Cluewren 自体は Cluewren のサーバーへデータを送信しません。 ゲームプレイ中の意思決定リクエストは TypeSafe の API へ送信され、エージェントはそれぞれのプロバイダーと通信します。

プラグインと SDK の導入

Cluewren 本体の導入手順は Cluewren のインストールガイド を、Unity プロジェクトへのプラグインの追加は Cluewren Unity プラグインのドキュメント を参照してください。 Framedash Unity SDK v0.1.8 は、Unity の Package Manager git URL から追加します。

https://github.com/crane-valley/framedash-unity-sdk.git#v0.1.8

計測スクリプトの追加

プロジェクトに次のスクリプトを追加します。

using System;
using Framedash;
using Unity.FPS.Gameplay;
using UnityEngine;

public sealed class FramedashPlaytestProbe : MonoBehaviour
{
    const string MapId = "fps-microgame-main";
    const float Interval = 0.25f;

    PlayerCharacterController _player;
    float _next;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    static void Boot()
    {
        if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FRAMEDASH_API_KEY")))
            return;
        TelemetrySDK.Initialize(buildId: Environment.GetEnvironmentVariable("FRAMEDASH_BUILD_ID"), enableOfflineQueue: false);
        TelemetrySDK.Instance.BeginAutomatedSessionFromEnvironment();
        var go = new GameObject(nameof(FramedashPlaytestProbe));
        DontDestroyOnLoad(go);
        go.AddComponent<FramedashPlaytestProbe>();
    }

    void Update()
    {
        if (Time.unscaledTime < _next)
            return;
        _next = Time.unscaledTime + Interval;
        if (_player == null)
            _player = FindAnyObjectByType<PlayerCharacterController>();
        if (_player != null)
            TelemetrySDK.Instance.Track("player_position", MapId, _player.transform.position);
    }

    void OnApplicationQuit() => TelemetrySDK.Instance.FlushBlocking(5000);
}

このスクリプトは FRAMEDASH_API_KEY が設定されている場合のみ SDK を初期化します。 FRAMEDASH_BUILD_ID と FRAMEDASH_TEST_SCENARIO から自動でビルドタグが設定されます。 プレイヤーの座標は 0.25 秒ごとに fps-microgame-main の map_id を持つ player_position イベントとして送信されます。 SDK の仕様により、すべての Track() イベントにはその瞬間のフレームタイム、FPS、GPU 時間、メモリが記録されるため、位置サンプルは場所ごとのフレームタイムのサンプルを兼ねます。 また、SDK は 10 秒ごとに perf_heartbeat を自動送信します。

環境変数の設定とエディタの起動

Unity エディタを起動する前に、次の環境変数を設定します。

FRAMEDASH_API_KEY=<events:write 権限を持つ Ingest キー>
FRAMEDASH_BUILD_ID=fps-walk-base
FRAMEDASH_TEST_SCENARIO=cluewren-walkthrough

エージェントがハーネスを作成して修正する初回実行には、比較からイベントを除外するため固有のビルド ID(今回は fps-walkthrough-baseline)を割り当てます。 その後の測定では、環境変数を設定した新しいエディタをそれぞれ起動し、ベースライン測定時に fps-walk-base、候補ビルド測定時に fps-walk-heavy-chamber を指定します。 API キーはエディタを動かす環境変数にのみ設定し、エージェントのチャットには貼り付けないでください。

エージェントへの指示と初回実行

エージェント(Claude Code、Opus)には、両方のビルドで同じ指示を与えます。

Walk the player through the whole level: start at the spawn point, go through every area the level contains, and finish back at the start. Do not fight unless an enemy blocks the way.

(プレイヤーを操作してレベル全体を巡回してください。スポーン地点から開始し、レベルに含まれるすべてのエリアを通過して、最後は開始地点に戻ってください。敵に行く手を阻まれない限り、戦闘は行わないでください。)

初回(コールドラン)の実行では、エージェントは 3 エピソードと 471 秒を要しました。 エピソード 1 ではフィールドの端から溶岩に落下し、エピソード 2 では HoverBot に倒されました。 エージェントはその都度ハーネスを修正し、エピソード 3 で巡回を完了しました。 この実行は固有のビルド ID である fps-walkthrough-baseline で記録されたため、そのイベントは比較に含まれません。 生成されたハーネスはプロジェクトの実行メモリに保持され、同じ指示に対して再利用されます。

二つのビルドでの実行結果

候補ビルドには、下層チェンバー(lower chamber)における負荷を模擬する処理を加えています。 プレイヤーがチェンバー内にいる間、メインスレッドが毎フレーム 12 ms スピンします。 これは重いアセットやエフェクトによる負荷をシミュレーションするためのものです。

保持されたハーネスを再利用したため、比較対象となる 2 回の実行はいずれも 1 エピソードで完了しました。

  • ベースライン(fps-walk-base): 成功(achieved)、レポート時間は 84.7 秒、開始から 62.0 秒でスポーン地点へ帰還。巡回ルートは中央ホール、西の祭壇、広いフィールドの 4 区画、フィールドのゲートウェイ、大扉を通る階段、下層チェンバーの入口・最深部・東側、スポーン地点への帰還。戦闘は 1 回のみ(攻撃してきた HoverBot を撃破、所要 2.8 秒)。
  • 候補ビルド(fps-walk-heavy-chamber): 成功(achieved)、レポート時間は 83.4 秒、計画された同じ 12 ステップを同じ順序で実行、開始から 62.4 秒でスポーン地点へ帰還。同じ 1 回の戦闘(2.8 秒)を実行。

perf-diff と Regression ページによる比較

二つのビルドの差分は、analytics:read 権限のキーを使用して Framedash CLI(@framedash/cli 0.2.0)の perf-diff コマンドで比較します。

npx @framedash/cli perf-diff --baseline fps-walk-base --candidate fps-walk-heavy-chamber --metric frame_time --threshold 10 --days 7

出力結果における各指標の測定値は次のとおりです。

  • frame_time: ベースライン P50 8.92 ms、候補ビルド P50 15.09 ms(+69.3 %、isRegression は true)。P95 は 52.66 ms に対し 53.20 ms。
  • memory: -0.4 %(性能回帰なし)。
  • gpu_time: -15.6 %(性能回帰なし。値が 1.4〜1.6 ms 前後でありノイズ)。

出力に含まれる isRegression は方向性を示すフラグであり、指標が悪化した際に常に true となります。 ここにしきい値判定は適用されません。 --threshold は、--fail-on-regression を指定した際の終了コードの制御にのみ使用されます。 CI でしきい値を超えた場合にジョブを失敗させるには、--fail-on-regression を追加します。 設定と運用については マージ前にゲームの性能を CI で比較する、Unity のパフォーマンス回帰を CI で検出する、および CLI リファレンス を参照してください。

ダッシュボードの Regression ページでも同じ結果を確認できます。

Framedash ダッシュボードの Regression ページに表示された、ベースラインと候補ビルドのフレームタイム比較
ダッシュボードの Regression ページ。fps-walk-base と fps-walk-heavy-chamber のフレームタイムを比較しています。

ダッシュボード上の数値も CLI と同様に、Frame Time が +69.3 %(8.92 ms から 15.09 ms)、サンプル数は両ビルドとも 299 件です。

クエリによるエリア別の内訳調査

性能回帰が発生した場所を特定するため、CLI の query コマンドで SQL を実行してエリア別に集計します。 このコマンドには data:admin 権限を持つ Full キーが必要です(analytics:read キーでは 403 エラーになります)。 下層チェンバーは、Unity のワールド座標でフロア面より下(y < -3)、かつ x > 20 のエリアです。

npx @framedash/cli query "SELECT build_id, (position_y < -3 AND position_x > 20) AS lower_chamber, count() c, round(quantile(0.5)(frame_time_ms),1) p50, round(quantile(0.95)(frame_time_ms),1) p95 FROM events WHERE build_id IN ('fps-walk-base','fps-walk-heavy-chamber') AND event_name='player_position' GROUP BY build_id, lower_chamber ORDER BY build_id, lower_chamber"

集計結果は次のとおりです。

build_idareasamplesP50P95
fps-walk-base下層チェンバー外2277.6 ms52.9 ms
fps-walk-base下層チェンバー6511.5 ms36.7 ms
fps-walk-heavy-chamber下層チェンバー外2247.0 ms53.2 ms
fps-walk-heavy-chamber下層チェンバー6826.0 ms54.0 ms

この結果から、性能回帰のすべてが下層チェンバー内で発生していることが分かります(P50 で +14.5 ms の悪化。毎フレームの 12 ms のスピン処理とオーバーヘッドに一致)。 それ以外の巡回ルートではフレームタイムに変化はありません。

測定における注意点

今回の手法を用いる際は、次の点に留意してください。

  • エディタとキャプチャの負荷: 今回の測定は Unity のエディタの Play モードで実行され、Cluewren がエージェント向けにフレームをキャプチャしています。両ビルドで P95 が約 53 ms となっているのはエディタおよびキャプチャのオーバーヘッドによるもので、プラットフォームは WindowsEditor と記録されます。この測定は同一条件下のビルド間における相対比較に用い、プレイヤー環境での絶対的な性能値としては扱いません。実機ビルドでの計測については ゲームの最適化を実機で確かめる を参照してください。
  • 実行条件の一致: 両方のビルドで同じ指示、同じマシン、同じウォーム状態のハーネスを使用し、測定条件を揃える必要があります。
  • API キーの管理: Ingest キーはゲームを起動する環境変数にのみ設定し、エージェントのチャットには絶対に貼り付けないでください(これは Cluewren が自身の TypeSafe キーに対して定めているルールでもあります)。

まとめ

エージェントが二つのビルドで同じ指示を実行し、Framedash がフレームタイムを記録します。 perf-diff コマンドとダッシュボードの Regression ページでどちらのビルドが遅くなったかを確認し、位置サンプルのクエリでレベル内のどの場所が遅くなったかを特定できます。 Cluewren の詳細については Cluewren 公式サイト を参照してください。