返回全部文章

用接近发布的构建验证优化:Unity、UE5 与 Godot 的性能测量流程

编辑器里的帧时间改善了,玩家设备上的游戏却未必更快。比较数字之前,先确定它们来自什么构建、什么负载。

判断发布性能,应在目标硬件上运行接近正式发布的构建。需要查找耗时原因时,换用具备诊断功能的构建;修复后,再回到接近发布的条件验证。本文把这种保留少量测量代码的构建称为测量构建。这是项目可以采用的约定,并不是三个引擎共有的构建配置名称。

根据问题选择构建

编辑器性能分析适合在开发中查找耗时脚本、资源处理和内存分配。带有诊断功能的独立构建(如 Unity 或 Unreal Engine 的 Development 构建)能排除编辑器进程的负载,并在目标设备上进行详细分析。但这些结果不能直接当作发布构建的性能。

编辑器:调查 诊断构建:设备分析 测量构建:验证

记录结果时附上构建配置。配置变化可能同时改变开销和被测行为。

编辑器窗口、资源处理和其他工具会共享设备资源。诊断构建还可能包含额外代码和性能分析开销。差异取决于引擎、后端、平台和启用的选项,没有一个固定数值可以直接扣除。在诊断构建中找到的高开销函数仍然值得调查,只是需要回到玩家实际运行的构建验证其影响。

改代码之前,先固定运行条件

单靠构建设置无法保证比较可复现。先选一个场景,例如读取存档后沿固定路线移动摄像机,并为每次结果记录以下条件:

  • 构建 ID、提交、引擎版本、脚本后端和构建配置
  • 设备、GPU 驱动、分辨率、画质设置、VSync 和帧率上限
  • 场景、初始状态、输入或路线、测量时长或帧数
  • 预热规则和缓存状态,尤其是着色器编译与首次加载的处理方式
  • 启用的测量代码、采样间隔,以及缺失或丢弃的样本

先把未修改的基线多跑几次,再测试候选构建。如果基线之间的波动已经接近预期收益,应先检查测试条件。重复运行有助于发现波动,但仅靠重复运行还不能证明结果在统计上可靠。手机和笔记本电脑还要保持相近的供电与温度条件。

首次加载与预热后的游戏过程应分开测试。测量稳定状态时,排除着色器编译可能是合理的;如果目标是调查首次启动卡顿,同样的处理却会把问题本身排除掉。

配置测量构建

Unreal Engine:引擎支持时使用 Test

Epic 将 Test 定义为保留部分控制台命令、统计和性能分析工具的 Shipping 配置。需要发布级优化和这些诊断工具时,可以从 Test 开始。Development 也启用了大部分优化,不能把它等同于未优化的 Debug 构建。

先检查引擎安装。Installed Build 默认包含 Shipping、Development 和 DebugGame。Test 通常需要源码构建的引擎,或包含 Test 的自定义 Installed Build。在命令中选择 Test,不会补上缺少的二进制文件。

支持 Test 的引擎可参考以下 Windows 打包命令。替换项目路径,并使用安装目录中的 Automation Tool 启动脚本:

RunUAT.bat BuildCookRun -project="C:/Path/MyGame.uproject" -platform=Win64 -clientconfig=Test -cook -stage -pak -package -build

在实际生成的包中确认所需命令和计数器是否可用。不要假设 UE_LOG 一定存在:Test 和 Shipping 默认会在编译时移除日志,更改该设置可能需要重新构建引擎。测量代码应限于项目中必要的位置。如果只能用 Development 调查,最后仍应使用项目支持的测量手段在 Shipping 中验证。

Unity:后端和编译设置与发布版本一致

在 Unity 6 中建立专用的构建配置文件,进行接近发布环境的比较时关闭 Development Build。产品使用 IL2CPP,就测 IL2CPP;使用 Mono,就保留 Mono。IL2CPP 的 Release 或 Master 配置也应与产品一致。比较代码修改时同时更换后端,会引入另一个变量。

为 Unity Profiler 保留独立的 Development 配置。连接 Profiler 和 Deep Profiling 是诊断选项,不应在发布性能比较中被无意保留。Unity 6 之前的版本可以用 BuildPipeline 脚本维持同样的区分。

可以使用配置专属的脚本定义符号,让诊断标记只在测量构建中生效:

using System.Diagnostics;

public static class Measure
{
    [Conditional("MEASUREMENT")]
    public static void Mark(string label)
    {
        UnityEngine.Debug.Log($"[measure] {label} @ {UnityEngine.Time.frameCount}");
    }
}

在配置的 Scripting Defines 中加入 MEASUREMENT,然后在测试场景的开始、结束等位置调用 Measure.Mark。这是日志标记,不是帧时间采集器。不要每帧调用:字符串格式化和日志写入也有开销。没有该符号时,[Conditional] 会移除调用点,但不会从程序集中移除方法体。

Godot:发布导出加测量功能标签

复制导出预设,关闭 Export With Debug。渲染器、平台和导出模板应与计划发布的版本一致。仅为创建测量预设,无需编译自定义引擎模板。

在预设中添加 measurement 等自定义功能:

custom_features="measurement"

C# 项目可在测试场景开始处读取标签:

if (OS.HasFeature("measurement"))
{
    GD.Print($"[measure] scenario-start frame={Engine.GetFramesDrawn()}");
}

自定义功能标签只对导出的项目生效,普通的编辑器运行不会使用它们。请检查导出的可执行文件。和 Unity 示例一样,这段代码仅标记场景位置,指标需要单独采集;采集开销和设置也应在各次运行中保持一致。

按实际采样粒度解读指标

60 FPS 的帧预算约为 16.67 ms,30 FPS 约为 33.33 ms。除了平均 FPS,还要检查帧时间分布。P95 是 95% 的观测值不超过的数值。更罕见的严重卡顿仍可能被隐藏,因此还应按需查看更高百分位、超出帧预算的次数和时间线。

先确认统计对象。定期上报的遥测样本算出的 P95,是这些样本的 P95,不一定代表每一个渲染帧。比较两个报告前,应记录采样方式。

帧时间记录测量来源和时间窗口,同时检查典型帧与长帧。
内存注明计数器和单位,观察峰值及反复加载、卸载的变化。数值较高不等于发生泄漏。
加载时间固定开始与结束的定义,首次加载和热缓存加载分开比较。
存储 I/O把读取和等待与帧时间线对照。吞吐量本身不能解释卡顿原因。

如果问题与位置有关,附上场景、坐标和摄像机方向。“站在门口、朝向物体密集的房间时出现慢帧”比整段会话的平均值更有助于复现,但还不能确定是哪个对象或系统造成了开销。

找到原因后,重跑原来的测试

按记录的条件复现问题,再用 Unity Profiler、Memory Profiler、Unreal Insights 或 Godot 的性能分析器调查。Godot 内置脚本性能分析器面向 GDScript;深入分析 C# 代码时,需要合适的 .NET 性能分析器。

修改一个疑似开销来源后,在测量构建中重跑原来的场景。确认多次运行仍有改善,并检查内存、加载或其他帧内工作是否恶化。诊断跟踪变快只是中间结果。

同样的数据也能帮助决定是否增加特效或对象。但渲染线程有余量,不代表 GPU 时间和内存也有余量。加入内容后,仍需在相同的目标设备条件下测量。

用 Framedash 保留可检查的比较结果

Framedash 的仪表板热力图可以在地图上查看已采集的性能样本,并按构建、平台和已记录的属性筛选。各引擎的 SDK 设置与可用采集器不同,请参阅 Unity、UE5 和 Godot C#。使用图表前,先确认目标构建实际发送了哪些指标。

Unity 用户可以从性能运行比较试点开始:在自己的 PC 上采集基线、未修改的重复运行和候选运行,再检查条件与帧间隔摘要,不需要地图。这些间隔测的是 SDK Update 回调之间的时间,不是 GPU 完成或画面呈现时间。试点会说明证据是否可比较,不会自动判定性能回归合格与否。

先选一个可复现的场景和一个待验证的修改。把基线结果、运行条件和候选结果放在一起,下一次才能继续回答同一个问题。