エディタでは滑らかに動いていたシーンが、実機に載せた途端にカクつく。 逆に、開発ビルドで重かった処理が、製品ビルドではあっさり流れることもあります。 同じゲームなのに、どの環境で測るかで数字が食い違うのはなぜか。
計測環境の 3 段階
パフォーマンス計測の環境は、大きく 3 段階に分けられます。 エディタ上でのプロファイリング、Development ビルドでのプロファイリング、そして Release / Shipping 相当の最適化を効かせたビルドでの計測です。 手前の段階ほど手軽に数字が出ますが、その数字はプレイヤーの手元で動く製品ビルドの数字から遠ざかります。
右へ進むほど計測ノイズが減り、プレイヤーが体感する数字に近づきます。
エディタ上の計測には、エディタ固有のコストが乗ります。 ホットリロードやアセット監視といったエディタ専用システムが裏で動き続け、Unity であれば IL2CPP ではなく Mono で回ります。 メモリもデバッグ用アロケータを経由するため、レイアウトやアラインメント、断片化の傾向、GC が走るタイミングが製品ビルドと一致しません。
Development ビルドにすると、エディタ固有のコストは消えますが、デバッグ用のオーバーヘッドは残ります。 アサーションチェックや冗長なログ出力、プロファイラー接続のようなデバッグ機能が有効なままで、コンパイラの最適化も本番と同じではありません。 インライン展開が抑えられていたり、LTO(リンク時最適化)が効いていなかったりします。 Development 構成のまま作ったパッケージに大量のログ出力やデバッグ機能が残っていて、その数字を製品の性能として読んでしまう例は珍しくないでしょう。
つまり手前の 2 段階で観測する「重さ」の多くは、計測環境そのものが生み出したノイズです。
プレイヤーの端末には存在しないコストを見て、実機にはないボトルネックと戦うことになります。
計測用ビルドの作り方
3 段階目にあたるのが、本番と同じ最適化条件で動く「計測用ビルド」です。 基本方針は、Release / Shipping 相当の最適化を効かせたうえで、計測に必要な最小限のログとフックだけを残すことです。 チート機能や過剰な診断は入れません。 あくまで「製品と同じ速度で動く、ただし数字だけは取れる」ビルドを目指します。
Unreal Engine:Test 構成を使う
Unreal Engine には DebugGame / Development / Test / Shipping というビルド構成があります。 ここで狙うのは Test 構成です。
Development は日々のイテレーション向けで、最適化はある程度効くものの、デバッグ機能やチェックが多く残ります。
逆に Shipping は製品出荷用で、コンソールコマンドや stat 系のツールまで削ぎ落とされ、計測フックを差し込みにくい構成です。
Test はその中間として設計されています。
Shipping と同等の最適化を保ちながら、stat unit などの診断コマンドや最小限のツールを使える状態を残します。
パフォーマンス計測に必要な速度特性と観測性を両立できるため、計測用ビルドの土台として素直な選択肢です。
ただし Test は、Development や Shipping のようにエディタのパッケージ化メニューからワンクリックで選べるわけではありません。 確実なのは、Automation Tool(BuildCookRun)か、Project Launcher のカスタムプロファイルから指定する経路です。
// MyGame.Build.cs (project module)
public class MyGame : ModuleRules
{
public MyGame(ReadOnlyTargetRules Target) : base(Target)
{
PublicDependencyModuleNames.AddRange(
new string[] { "Core", "CoreUObject", "Engine" });
// Measurement-only compile switch, scoped to this module.
// Module-level PrivateDefinitions leave the shared (installed-
// engine) build environment untouched, unlike target-level
// GlobalDefinitions, which would require a unique build
// environment and abort UBT on Launcher engines.
if (Target.Configuration == UnrealTargetConfiguration.Test)
{
PrivateDefinitions.Add("MEASUREMENT_BUILD=1");
}
}
}
RunUAT BuildCookRun -project="C:/Path/MyGame.uproject" -platform=Win64 -clientconfig=Test -cook -stage -pak -package -build
なお、Test でも UE_LOG は既定でコンパイルアウトされます(有効化スイッチの bUseLoggingInShipping は Test と Shipping の両方に効きます)。
ただしこの設定は Launcher 版(installed)エンジンでは unique build environment を要求して UBT が中断するため、計測フックの出力は UE_LOG に依存しない形にしておくのが安全です。
また、Launcher 版エンジンには Test 用のエンジンバイナリが既定で含まれません(installed build の GameConfigurations 既定は Shipping、Development、DebugGame です)。 Test でパッケージできない場合は、ソースビルドのエンジンか、Test を含めてビルドしたカスタム installed build を使います。
この構成は UE5(および UE4)で共通して使えます。
Unity:計測用ビルドプロファイル(Unity 6)
Unity 6 では Build Profiles(ビルドプロファイル)という仕組みが導入されました。 プラットフォームごとのビルド設定を名前付きプロファイルとして保存し、切り替えられる機能です。 計測用に専用のプロファイルを 1 つ切り、次の方針で構成します。
- Development Build のチェックは外す(プロファイラー自動接続やスクリプトデバッグのオーバーヘッドを乗せない)
- スクリプティングバックエンドは IL2CPP を選び、製品と同じネイティブコード経路で計測する
- Scripting Define Symbols で計測用の最小ログだけを有効化し、それ以外の冗長ログは切る
具体的な手順はこうです。
File > Build Profiles から計測用のプロファイルを新規作成し、Development Build のチェックを外します。
Player Settings では Scripting Backend を IL2CPP、C++ Compiler Configuration を Release にします。
計測用の MEASUREMENT 定義は、プロファイルの Build Data > Scripting Defines に追加します(プロファイルの定義は、プロジェクトやプラットフォームの定義に加算されます)。
計測コードの呼び出しは、この定義が有効なビルドでだけコンパイルに残ります([Conditional] が取り除くのは呼び出し側で、メソッド本体はアセンブリに残ります)。
using System.Diagnostics;
public static class Measure
{
// Call sites are stripped at compile time unless the MEASUREMENT
// define is active for the current build profile. The method body
// itself stays in the assembly.
[Conditional("MEASUREMENT")]
public static void Mark(string label)
{
UnityEngine.Debug.Log($"[measure] {label} @ {UnityEngine.Time.frameCount}");
}
}
計測のデフォルトは Release を薦めます。
スタジオが Master 構成で出荷するなら、計測も Master で行います。
[Conditional] による切り替え自体は Unity 6 より前でも、次に述べる BuildPipeline の経路で同じように使えます。
Unity 6 より前のバージョンには Build Profiles はありません。
その場合は、同等のリリース構成を組むビルドスクリプト(BuildPipeline で Development フラグを立てず、IL2CPP と最小ログのシンボルを指定する)を用意して代替します。
Godot:リリースエクスポートとカスタム機能タグ
Godot では、エクスポートプリセットがこの役割を担います。 「Export With Debug」を外して(つまりリリース用エクスポートテンプレートで)エクスポートすると、リモートデバッガーやプロファイラー接続といったデバッグ機構が外れた、最適化済みのバイナリになります。
計測用にはプリセットを 1 つ複製し、カスタム機能タグ(例:measurement)を付けておきます。
コード側は OS.HasFeature("measurement") で分岐できるため、計測用の最小ログだけをこのビルドで有効化できます。
# export_presets.cfg (measurement preset section)
custom_features="measurement"
// Enable minimal measurement logging only in the measurement build.
if (OS.HasFeature("measurement"))
{
GD.Print($"[measure] frame={Engine.GetFramesDrawn()} fps={Engine.GetFramesPerSecond()}");
}
カスタム機能タグが解決されるのは、エクスポートしたビルドの中だけです。
エディタから実行している間は OS.HasFeature("measurement") が false を返すため、エクスポートするまでこの分岐は動かないコードのように見えます。
バイナリをさらに小さく速くしたい場合は、production=yes でビルドした自作のエクスポートテンプレートに差し替えることもできます。
自作テンプレートのビルドは scons platform=windows target=template_release production=yes module_mono_enabled=yes で、production=yes は use_static_cpp=yes debug_symbols=no lto=auto のエイリアスです(Godot 4.x)。
C# プロジェクトで使うテンプレートには module_mono_enabled=yes が必須です。
C# 対応のテンプレートはこの 1 行だけでは完成せず、mono glue の生成と GodotSharp マネージドアセンブリのビルドも必要です(手順は公式ドキュメントを参照)。
計測用ビルドで計測する指標:フレームタイム、メモリ、ロード時間、ストレージ IO
計測用ビルドが用意できたら、実際のプレイセッションを通して数字を集めます。 見るべき指標は次のとおりです。
数字には、状況コンテキストを必ず添えます。 具体的には、そのときのプレイヤー座標、カメラの向き、表示中のマップ / シーン、ビルド ID、端末やプラットフォームです。
コンテキストのない「フレームタイムが跳ねた」という記録は、後から追いようがありません。 一方「build 1042 の desert_ruins、座標 (X, Y)、カメラが北向きのときに P95 が跳ねる」という粒度で残っていれば、その異常は再現可能な調査対象になります。
コンテキストこそが、ただの数字を「調査に着手できる異常」に変える鍵です。
ヘッドルームの使いみち
計測の目的は、遅い箇所を直すことだけではありません。 ターゲットハードウェア上で CPU、GPU、メモリ、IO にまだ余裕(ヘッドルーム)が残っているなら、その余裕はそのまま使える予算になります。
たとえば描画スレッドに余力があるなら、VFX をリッチにする、同時表示オブジェクトを増やす、ストリーミング距離を伸ばすといった方向に、あえて予算を振れます。 最適化は削るための作業であると同時に、どこまで表現を積めるかを教えてくれる作業でもあります。
計測用ビルドで正確なヘッドルームを把握していれば、こうした判断を勘ではなく数字で下せます。 余っているのか、すでにギリギリなのか。 それが分かって初めて、ゲームプレイの密度を意図して設計できます。
異常から、原因へ
計測用ビルドの役割は「どこで、いつ」問題が起きるかを特定することです。 原因そのものは、ここでは分かりません。
たとえば「特定座標でフレームスパイクが出る」「あるロードの後にメモリが一段上がって戻らない」といった異常が計測用ビルドから挙がったとします。 次のステップは、まさにその状況を Development ビルドで正確に再現し、詳細なプロファイラーにかけて内訳を追うことです。
- Unity なら Unity Profiler と Memory Profiler
- Unreal Engine なら Unreal Insights
- Godot なら組み込みプロファイラーとモニター(スクリプト計測は GDScript のみのため、C# コードの計測には .NET プロファイラーを併用します)
ここで「なぜ」が見えてきます。 どのシステムがそのフレームを食っているのか、どのアセットがメモリを掴んだまま離さないのか。
この 2 段構えが、憶測に頼らない最適化ループになります。
Framedash で、計測ループを自動化する
ここまでの「集める → 集計する → 異常を見つける」という流れは、手作業で回すと運用コストがかさみます。 Framedash は、このループの自動化を担うツールです。
FPS、フレームタイム、GPU 時間、メモリ使用量を、ゲームマップ上にセル単位でオーバーレイするパフォーマンスヒートマップを提供します。 デバイスやビルドプロファイルで絞り込めるので、どの端末構成のどこが重いかを一目で把握できます。
各テレメトリイベントにはプレイヤー座標とカメラ向き(ヨーとピッチ)を付与できます。 カメラ向きは SDK が自動で収集するため(オプトアウト可)、本記事で述べた「状況コンテキスト付きの計測」を追加実装なしにそのまま実践できます。 任意のカスタムメトリクスや属性を添えることもできます。 各エンジンの SDK 統合手順は、Unity、UE5、Godot (C#) のドキュメントにまとまっています。
さらに build_id ごとにフレームタイム、GPU 時間、メモリを比較するビルド回帰分析を備え、しきい値を超えた回帰は Slack、メール、Webhook へ通知できます。 リリース前に「このビルドで P95 が悪化した」と気づける仕組みです。