ロード画面が消えるまで待った時間と、LoadSceneAsync が完了するまでの時間は、同じとは限りません。
シーンを読み込んだ後も、セーブデータの適用やプレイヤーの生成、入力の有効化が残ることがあります。
ロードを短くしたいなら、まず 計測を止める条件 を決めます。
この記事では、ロード要求から操作可能になるまでを全体の指標とし、その途中を診断用の時刻で分けます。 Unity 6.4 の API と Framedash Unity SDK 0.1.8 の仕様に沿った計測設計です。
進捗 0.9 と操作可能を分ける
allowSceneActivation = false にすると、Unity は進捗を 0.9 で止め、isDone を false のままにします。
これはシーンを有効化する前の待機点で、読み込んだバイト数の 90% や CPU 処理時間の 90% を意味しません。
有効化を許可するまで、他の AsyncOperation が待たされる場合もあります。
Unity の allowSceneActivation の説明を踏まえ、計測のためだけにこの待機を追加しないでください。
また、sceneLoaded は OnEnable の後、Start の前に呼ばれます。
その通知を操作可能の印にすると、Start や後続の非同期初期化が計測から外れます。
Unity のコールバック順序と、ゲーム側の準備完了を別々に扱います。
同じ時計で 4 つの時刻を記録する
- T0:ロード要求。ローダーを呼び出す直前
- T1:有効化の許可。既存ローダーが有効化を止めている場合に、
allowSceneActivationを true にする直前 - T2:シーン操作の完了を観測。
LoadSceneAsyncのisDoneが true になったことを確認した時点 - T3:操作可能。必要な初期化が終わり、ロード画面を外して入力を受け付けるという、ゲーム側で決めた条件が成立した時点
T0 から T1 には、読み込みだけでなく、演出や「続ける」ボタンを待つ時間も入り得ます。 T1 から T2 も、有効化の純粋な CPU 時間ではありません。 メインスレッドの別の処理や、コルーチンが次に確認するまでの時間を含む経過時間です。 原因を処理単位で調べる段階では Unity Profiler を併用します。
有効化を手動で止めていないローダーには、T1 を設ける必要はありません。
T0 から T2 と T2 から T3 を記録するだけでも、シーン操作の後に待たせている時間を分けられます。
準備完了が T2 より先に通知され得る構成では、その状態を呼び出し側で保持し、T2 を観測した後に TryReportPlayable を呼び直します。
この補助クラスは早い通知を保持しません。T3 は、両方の条件がそろったことを観測して報告する時点です。
Framedash には全体を 1 件、内訳を別イベントで残す
map_load に記録する区間は、比較するビルド間でそろえます。
ここでは T0 から T3 を 1 件のロードとして記録します。
前のビルドでは T2、新しいビルドでは T3 で終了すると、処理が同じでも計測値が増えてしまいます。
Framedash のロード計測 APIには BeginMapLoad / EndMapLoad と ReportMapLoad があります。
開始と終了を SDK に任せる場合は前者、既存ローダーで測ったミリ秒を渡す場合は後者を使います。
BeginMapLoad を重ねて呼ぶと保留中の計測が置き換わるため、同時ロードを別々に測る場合は、それぞれの時計と ReportMapLoad を使います。
次の補助クラスは後者の例です。
初期化済み SDK を渡し、T0 でインスタンスを作ります。
既存ローダーから T1 で任意の MarkActivationRequested、T2 で MarkOperationDone、T3 で TryReportPlayable を呼びます。
シーン切り替えで破棄されないローダーが、このインスタンスを保持する想定です。
サンプルの仕様は SDK 0.1.8 の実装と照合しています。 呼び出しは Unity のメインスレッドで行います。 バックグラウンド処理の完了時は、ゲームのメインスレッドへ戻してから報告してください。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Framedash;
// One instance per load. Call every method on Unity's main thread.
public sealed class LoadProbe
{
private readonly TelemetrySDK sdk;
private readonly string mapName;
private readonly Stopwatch clock = Stopwatch.StartNew();
private double? activationRequestedMs;
private double? operationDoneMs;
private bool reported;
public LoadProbe(TelemetrySDK initializedSdk, string mapName)
{
if (initializedSdk == null)
throw new ArgumentNullException(nameof(initializedSdk));
if (string.IsNullOrWhiteSpace(mapName))
throw new ArgumentException("A stable map name is required.", nameof(mapName));
sdk = initializedSdk;
this.mapName = mapName;
}
// Optional: only if the existing loader controls activation.
public void MarkActivationRequested()
{
if (!reported && !activationRequestedMs.HasValue && !operationDoneMs.HasValue)
activationRequestedMs = clock.Elapsed.TotalMilliseconds;
}
public void MarkOperationDone()
{
if (!reported && !operationDoneMs.HasValue)
operationDoneMs = clock.Elapsed.TotalMilliseconds;
}
// Call when both readiness and isDone have been observed.
// An early readiness call is not cached; call again after MarkOperationDone.
public bool TryReportPlayable()
{
if (reported || !operationDoneMs.HasValue) return false;
double totalMs = clock.Elapsed.TotalMilliseconds;
double doneMs = operationDoneMs.Value;
var metrics = new Dictionary<string, float>
{
{ "scene_operation_ms", (float)doneMs },
{ "after_operation_ms", (float)(totalMs - doneMs) }
};
if (activationRequestedMs.HasValue)
{
double activationMs = activationRequestedMs.Value;
metrics["before_activation_ms"] = (float)activationMs;
metrics["activation_window_ms"] = (float)(doneMs - activationMs);
}
reported = true;
sdk.ReportMapLoad(mapName, totalMs);
sdk.Track("load_phases", attributes: new Dictionary<string, string>
{ { "map_name", mapName } }, metrics: metrics);
return true;
}
}
TryReportPlayable の true は、この補助クラスが報告処理を 1 回行ったという意味です。
サーバーへの到達確認ではありません。
失敗やキャンセルでは呼ばず、その試行を成功ロードの集計に入れないでください。
必要なら別の失敗イベントを用意します。
load_phases とそのメトリクス名は、この記事で定義したカスタムイベントです。
専用の区間グラフや回帰ゲートが自動で作られるという意味ではありません。
map_load の map_id は空で、マップ名は attributes["map_name"]、時間は metrics["load_time_ms"] に入ります。
このイベントだけでは空間ヒートマップの点になりません。
どちらのイベントもサンプリングの影響を受けます。 検証時に全件を保持する設定が必要なら、初期化後に次を設定します。 ただし、レート 1 でも通信障害やバッファの制約による欠落までは防げません。
TelemetrySDK.Instance.SetEventSamplingRate("map_load", 1f);
TelemetrySDK.Instance.SetEventSamplingRate("load_phases", 1f);
比較前に、終了条件と失敗ケースを確かめる
自分のローダーへ組み込むときは、次の点を確認してください。
- 同じシーン、同じビルド設定で T0、T2、T3 の順序と単位を確認する
- 有効化を止める既存フローでは、意図した待ち時間が T0 から T1 に含まれることを確認する
MarkOperationDoneの後に非同期の準備完了を意図的に遅らせ、T2 から T3 が長くなることを確かめる。Awakeなどを遅らせるテストとは分ける- 失敗、キャンセル、二重完了、同時ロードで、成功件数の水増しや別ロードへの混入がないことを確認する
- コールド起動と再ロードを分け、端末、ストレージ、キャッシュ条件、ビルド ID を記録する
サンプルの確認範囲
この補助クラスは、Unity 6000.3.25f1(6.3 LTS)と 6000.6.4f1、Framedash Unity SDK 0.1.8 で動作を確認しています。
同じ Windows x64 ホストで、Mono / IL2CPP の両方を、非 batchmode の D3D12 非表示ウィンドウと -batchmode -nographics で実行しました。
SDK と LoadProbe に変更を加えず、記事サンプル用の合成シーンテストが成功し、SDK の終了処理後に Player が正常終了しています。
このバージョン検証では、map_load と load_phases の実際の送信データをローカルでデコードして受信を確認しました。
失敗・キャンセルした試行は成功ロードとして送信されていません。
本番 Framedash への疎通は別の試行で確認しており、保存済みの両イベントを読取クエリで取得できました。
いずれも確認した試行の結果で、すべてのイベントの到達を保証するものではありません。
検証対象はこの記事のサンプルです。SDK 全体のテストスイート、顧客実ゲームの性能、可視の対話ウィンドウ、Android / iOS 実機まで確認したものではありません。 自分のゲームでは、上のチェック項目に加えて対象端末での動作を確かめてください。
まず T0 から T3 の定義を固定し、次に長い区間を調べます。 その順序なら、読み込み処理を短くしたのか、プレイヤーが待つ時間を短くしたのかを、同じ指標で確かめられます。