返回所有文章

用 Cluewren 自動遊玩測量 Unity 的效能退化

為每個建置手動沿著相同路線遊玩來測量效能既費時,又難以在建置之間進行準確比較。 透過開源(Apache-2.0)的 Cluewren,開發者自己的程式碼撰寫代理(Claude Code 或 Codex)可經由 Cluewren 的 MCP 伺服器,直接在 Unity、Godot 或 Unreal Engine 編輯器中遊玩已開啟的遊戲。 代理讀取程式碼並編寫 C# 測試控具(harness),TypeSafe 快速決策模型 Jev 透過引擎外掛執行遊玩,最後由代理結合實測證據報告判定結論。 在此遊玩過程中,Framedash Unity SDK 會記錄影格時間。 本文透過在兩個建置中走完相同路線,找出哪個建置以及關卡的哪個區域變慢了。 候選建置中的效能降低是作為高負載資產或特效的替代品,透過模擬方式產生的。

使用環境與先決條件

本次驗證使用了以下環境:

  • Unity:Unity 6000.6.7f1
  • 範例專案:安裝了 Cluewren Unity 外掛的 Unity FPS Microgame(MainScene)專案複本
  • 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 秒傳送一次,作為 map_id 為 fps-microgame-main 的 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 個回合(episodes)與 471 秒。 在第 1 回合中,它從場地邊緣掉入了熔岩;在第 2 回合中,它被 HoverBot 擊倒。 代理每次都修正了自己的測試控具,並在第 3 回合順利走完全程。 由於本次執行記錄在專屬的建置 ID fps-walkthrough-baseline 之下,因此其事件不會包含在比較中。 產生的測試控具會保留在專案的執行記憶體中,並在收到相同請求時重複使用。

兩個建置的執行結果

候選建置在下層密室(lower chamber)中模擬了高耗損效果:當玩家處於密室內時,主執行緒每影格自旋(spin)12 ms。 這是對實際沉重資產或特效負載的替代模擬。

由於重複使用了已暖機的測試控具,用於比較的兩次執行各僅花費了 1 個回合:

  • 基準(fps-walk-base):達成目標(achieved),報告耗時 84.7 秒,於 t=62.0 秒時回到重生點。巡迴路線依序為中央大廳、西側祭壇、寬廣場地的 4 個區塊、場地通道、穿過大門的階梯、下層密室入口·盡頭·東側,最後回到重生點。戰鬥僅發生 1 次(擊倒向其射擊的 HoverBot,耗時 2.8 秒)。
  • 候選建置(fps-walk-heavy-chamber):達成目標(achieved),報告耗時 83.4 秒,以相同順序執行了相同的 12 個計畫步驟,於 t=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 設定與流程,請參閱 合併前,先用 CI 比較遊戲效能、在 CI 中偵測 Unity 效能退化 與 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 金鑰(唯讀金鑰會收到 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_id區域樣本數P50P95
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 官網。