关于 scenario-unity-performance
面向 Unity 性能工程的技能,通过 Editor 分析、开发构建和性能捕获定位瓶颈,并用目标设备上的前后数据验证优化。覆盖脚本、渲染、内存、加载卡顿与构建体积。
主要功能
- 01建立预算并记录基准数据
- 02分析开发构建中的帧与性能捕获
- 03归因脚本、渲染和内存开销
- 04检查卡顿、GC 分配与 GPU 负载
- 05逐项验证优化并守护性能预算
适用场景
- 01定位目标设备上的 CPU 或 GPU 瓶颈
- 02调查加载、生成或着色器编译造成的卡顿
- 03减少每帧分配并检查内存预算
- 04对比优化前后的帧时间和构建体积
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ scenario-labs/ skills --skill scenario-unity-performance安装后在 Claude Code 中输入/scenario-unity-performance确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/critique.mdexpert-notes.mdgui-paths.mdprocedures.mdsources.mdscripts/ut_perf.pyscripts/AgentKit/Performance/PerfAudit.csPerfBuild.csPerfCapture.csPerfFrameDebug.csPerfLod.csPerfRendering.csPerfShaders.csscripts/Runtime/Performance/AgentKit.Performance.Runtime.asmdefAllocScope.csPerfProbe.cs详细介绍
Unity 性能(性能工程师)
专家级性能工作是一门测量学科:以毫秒设定预算,在做出任何改动前明确瓶颈线程,优先解决测得的最大开销,先调整结构再进行调优,并用开发版 Player 的前后数据证明每项修复。Agent 可无界面完成所有工作:在 Editor Play 模式下使用 AgentProfile 迭代,在批处理模式的 Player 中使用 PerfProbe 得出结论,通过 PerfCapture 从 Profiler 捕获数据中归因开销,使用 metal_trace 测量 GPU 时间,使用 PerfBuild 测量大小。目标环境:Unity 6000.3.21f1、URP 17.3、macOS Apple Silicon。如果此处提到的同级技能不在可用技能中,请让用户安装
(npx skills add scenario-labs/skills --skill <name>);若无人值守,请根据工具架构继续操作并标明缺失项。
必备背景: scenario-unity-expert(通道、评审循环、6.3 陷阱、ut_* 工具包)。
立场(专家要点)
- 动手前先明确瓶颈。“如果你的性能分析结果是这样的,那么加快 GPU 也不会让运行速度更快”(Unity Profiler 团队的 Peter Hall,_cV1B2hqXGI [00:05:50])。观察结果:每帧移除 1.36 MB 垃圾,并未改善受 5,946 次 SetPass 调用限制的帧。GPU 瓶颈需要 GPU 计时;Frame Debugger 不提供该数据(Hall [frame 00:09:18])。
- **以 Player 为准,用 Editor 迭代。**权威依据是在最低目标设备上运行的开发版构建(性能分析电子书,第 18 页)。GRD:在包含 200 万个实例的压力测试中提升 4.4 倍,在 Editor 中提升 2.07 倍,而在实际游戏的独立运行版本中仅提升 1.45 倍(Oc6T4hh5gaI [frame 00:12:41]);这里的 Editor 测试结果从提升 1.5 倍到 3.0 倍不等,Player 在压力测试场景中则从提升 1.9 倍到 2.8 倍不等。预计提升约 1.5 倍。
- **GC.Alloc 样本时间不等于实际开销。**Unity 只记录时间戳和大小(电子书,第 27 页):54,021 次分配显示为 1.01 ms,却导致了 19.97 ms 的
GC.Collect(Hall [frame 00:21:52])。观察结果:每帧 27,000 次分配显示为 0.8 至 1.1 ms;产生分配的代码实际耗时 3.8 至 5.4 ms。统计字节数和调用次数,测量垃圾回收,目标是每个游戏帧分配 0 B,并确保探针本身不产生分配(git-amend ham_w48aRJ4 [00:11:30])。 - **6.3 URP 绘制调用 = SRP Batcher + GPU Resident Drawer;2024 年的建议仅适用于 Built-in。**材质上的“Enable GPU Instancing”关闭,静态批处理关闭,不使用 MaterialPropertyBlock(6.3 手册表格)。除非将 BatchRendererGroup Variants 设为 Keep All,且 URP 资源的每个渲染器都是 Forward+ 或 Deferred+,否则 GRD 会静默地不执行任何操作(根据观察得出的 URP 17.3 源码结果)。随后证明覆盖情况:GRD 绘制在 Frame Debugger 中显示为“Hybrid Batch Group”(仅限 GUI Editor);其他每次绘制都对应一个被排除的渲染器(MPB、LPPV、实时 GI、非 DOTS 着色器、蒙皮;6.3 手册);无界面运行时,请在
Start之后逐个检查渲染器是否符合条件。 - **先调整数据布局;将工作分摊到多个帧只会掩盖开销。**20 万个对象:通过管理器、结构体、Burst 和 SoA,将 MonoBehaviour Update 从 27 ms 降至 0.13 ms;“这只是在把工作分摊到各处”(Jason Booth,NAVbI1HIzCE [00:06:38]-[00:18:17])。
- **修复卡顿前先完成归因,并在代码中保留标记。**流式加载和生成系统都应带有各自的 ProfilerMarkers 和计数器;Deep Profile 只能找出填补空白的部分(Hall [00:18:39]-[00:19:12])。编译会显示为
Shader.CreateGPUProgram(加载时预热经跟踪的 GraphicsStateCollection,Nicolas Borromeo CmD8MVGkDxQ [00:11:14]-[00:14:35]);这里的预热移除了所有 18 次首次使用时的编译,但第一帧仍在Mono.JIT上耗时约 250 ms。 - **常驻内存会造成致命影响;未跟踪内存通常不会。**0.75 GB 未跟踪内存实际有 140 MB 常驻(Borromeo [00:32:22]);Editor 的内存用量约为 Player 的 5 倍(Uuzd39AjFWQ [00:10:03])。在笔记本电脑上,页面调度和 VRAM 溢出本身就会导致卡顿。
- **计数准确,时间会波动[新增,基于观察]。**在共享机器上(此处负载平均值为 50 至 210),交错运行 A/B 测试,并要求结果的变化方向一致。
首先明确
确定目标平台和各档位的最低规格设备;帧预算(桌面端 16.67 ms,移动端为其 0.65 倍);分辨率和质量档位;允许变更的范围(仅代码,还是也包括美术、设计、手感);可重复的基准测试;评估流式加载时使用基于时间的测试(速度提升后,固定的每帧步长会更早到达边界)[added];脚本后端和构建类型;实际进行测量的机器(独立 GPU、接通电源;探测 JSON device)[added]。默认值:PC 档位 URP 资源、60 fps、1920x1080、用于迭代的开发版 Mono 玩家,用于控制体积的发布版 IL2CPP。
工作流
- 设定预算和基准测试。 GATE:基准 CSV。
- 在编辑器中迭代。
ut_perf.profile(render=False用于隔离脚本)、ab_profile用于测量时间差。GATE:数值标注为“编辑器,迭代”。 - 静态基线。
PerfRendering.Audit(批处理、每个渲染器的 GRD 适用性、各平台的图形作业、Build Profile、堆栈跟踪、上传缓冲区、剔除)、lint_scripts、Project Auditor(project_auditor,先处理 Major,triage_auditor保留逐帧行)、PerfAudit.AudioImport、bundle_duplicates。GATE:按调用频率对发现的问题进行分类。 - 测量玩家版本。
build_player(development=True)、run_player(..., shot=png, markers=[...])。GATE:draw_calls> 0,PNG 已打开,budget_check。 - 判断性能瓶颈。 捕获等待标记(
capture_player、analyze_capture)、最长帧与中位帧。GPU:metal_trace+metal_gpu_frames(GPU 占用情况和各渲染通道的毫秒数;在 GUI 中使用 Rendering Debugger Display Stats),然后进行像素受限测试(4K 与 1080p、渲染比例)。GATE:明确瓶颈并给出数值。 - 归因。
attribute_spikes(你的标记、GC.Collect、编译、MeshCollider 烹制、同步读取、物理步进)、physics_catchup、memory_budget、density_check、每个代码块的分配量(AllocScope、Is.Not.AllocatingGCMemory())、GRD 覆盖率(PerfFrameDebug.Capture或窗口)。GATE:明确列出成本最高的三项,没有无法解释的尖峰。 - 每次只修复一项,优先调整结构。 脚本:分配、管理器,然后是 Burst。CPU 渲染:Material Variants、SRP Batcher 着色器、
EnableGpuResidentDrawer、单个摄像机、在 DX12/Vulkan 上拆分图形作业。GPU:动态分辨率或 STP、深度预通和减少透明效果、在三角形已达到像素大小时使用 LOD、将后期处理固定在特定时间片上;光照使用 scenario-unity-rendering-lighting。卡顿:异步加载、预热对象池、在加载阶段预热 PSO、烹制后的 MeshColliders、音频加载类型、避免重复的资源包依赖。GATE:compare_line显示目标指标下降,其他指标均未上升,图像保持一致。 - 发布配置。 发布版 IL2CPP,选择剥离和代码生成方式,并根据已使用的集合剥离变体(P15);通过
BuildPlayerOptions设置标志(平台配置文件共享设置)。GATE:构建成功,体积原因明确,冒烟测试运行正常。 - 守护。 每次提交都运行分配测试和 Performance Testing 示例组,并对已更改的脚本运行 lint。GATE:测试通过。
数值
| 数值 | 对应指标 | 来源 |
|---|---|---|
| 16.67 / 33.33 ms;移动端为 x 0.65 | 60 / 30 fps 下的帧预算;热余量 | profiling e-book 第 9、36 页 |
| 0 B | 每个游戏帧的托管分配量 | 6.3 Manual;e-book 第 66 页 |
| 最高约 1 ms | 增量 GC 写屏障的每帧开销 | e-book 第 69 页 |
| 4.4x / 2.07x / 1.45x | GRD 提升:200 万实例压力测试 / 在 Editor 中运行游戏 / 独立运行同一游戏 | Oc6T4hh5gaI [帧 00:12:41] |
| 217 / 199 / 176 / 161 fps | DX12 Split / Legacy / 纯 DX12 / Native graphics jobs | Oc6T4hh5gaI [帧 00:03:52] |
| 256 个顶点 | 低于此值时 GPU 实例化效率低:应改为合并 | 6.3 Manual |
| 约 250 ms | 创建一个 PSO 时的卡顿 | Oc6T4hh5gaI [帧 00:04:48] |
| 4 次像素着色器调用 | 每个单像素三角形(2x2 四边形);目标为每个渲染像素绘制 1 个三角形 [已添加] | CmD8MVGkDxQ [00:27:26] |
| 48.55 / 80 ms | 开发帧中每帧调用 Debug.Log 的开销 | xjsqv8nj0cw [00:10:23] |
| < 200 / > 400 KB | 音频:Decompress On Load / Streaming | console e-book 第 104-105 页 |
| 最高 1 ms | 移动端每增加一个活动摄像机的 CPU 开销 | console e-book 第 89 页 |
| 最高 10 s | Autoconnect Profiler 增加的启动时间 | e-book 第 47 页 |
| RAM 的 50% / 70% / 80% | 内存预算:2 GB 手机 / 通用设备 / 专用设备 | Hall;Unity 教程;e-book |
| 5,946 降至 33 次 SetPass;58-60 降至 5.2-6.2 ms | 3,000 个球体,将 MPB 色调改为 8 个 Material Variants,开发版播放器 | 实测 |
| 18 降至 0 | 预热 10 个 PSO 后,首次使用时的 Shader.CreateGPUProgram(加载时为 3.6 ms) | 实测 |
| 177.9 / 110.5 / 116.3 MB | macOS 应用:IL2CPP Minimal + speed / IL2CPP High + size / Mono | 实测 |
| 1,475 万降至 26 万个三角形(每像素 7.1 降至 0.12 个) | 对 400 个高密度网格应用 Mesh LOD 后,绘制调用数不变 | 实测 |
帧耗时 57.1 ms = Spawn.Burst 26.9 ms + 4 次编译;下一帧有 3 次物理步进 | 突发实例化 3,000 个对象,耗时记录归于其自身标记 | 实测 |
| 48-80 ms 对比 0 ms | 生成时烹制 MeshCollider,对比加载时在作业中调用 Physics.BakeMesh(40 个网格) | 实测 |
| 33 us 对比 2.6-3.9 us | 每次 Debug.Log 的开销;开发版播放器中,堆栈跟踪设为 ScriptOnly 对比 None | 实测 |
| 2.5 对比 5.8 ms;像素数增加 4 倍时为 1.2x | GPU 忙碌时间对比 CPU 主线程时间(Metal System Trace);4K 对比 1080p:不受像素数限制 | 实测 |
| 1,944 降至 1,002 KB | 两个共享纹理的资源包:隐式副本对比共享资源包 | 实测 |
质量门槛
- **可测量:**玩家
budget_check通过(p95 低于预算,且无游戏卡顿);玩家中的gc_check通过;未解释的attribute_spikes= 0;除强制尖峰外,physics_steps至多为 1;memory_budget通过;density_check通过;PerfRendering.Audit的 counts.error == 0,且所有 GRD 后续影响均有说明;加载后Shader.CreateGPUProgram为 0;代码中没有新增的 Major Project Auditor 问题;IL2CPP 构建在预算范围内。 - **视觉:**玩家的最后一帧(
-perfShot)和使用相同书签拍摄的编辑器前后截图、ut_review.compare,并已打开联系表;没有空白、纯色或洋红色画面。
常见错误
| 错误 | 表现 | 修正方式 |
|---|---|---|
| 优化了未受限的一侧 | 分配减少,帧时间不变 | 先分类(等待标记、GPU 忙碌、SetPass) |
| 轻信 Editor 数值或单次运行 | “快了 3 倍”的优势在播放器中消失 | 使用播放器 CSV;以一致的符号约定运行 ab_profile |
| 对不渲染内容的批处理模式播放器进行基准测试 | 帧时间为 0.3 ms,绘制调用为 0 | 执行 PerfProbe 提交 + -perfShot |
| 开启 GRD 后毫无变化 | 绘制调用数不变 | Keep All、Forward+/Deferred+、关闭静态批处理,并修复失效项 |
| 用 MaterialPropertyBlock 设置对象颜色 | SetPass = 绘制调用数;StdRender.ApplyShader 占主导 | 使用 Material Variants;搭配 GRD 使用 unity_RendererUserValue |
| 把 GC.Alloc 的毫秒数当作开销 | “分配耗费了 0.5 ms” | 统计次数和字节数、GC.Collect 的毫秒数,并进行 A/B 测试 |
| 每帧都会格式化字符串的监视器 | GC 列始终不为 0 | 使用预分配缓冲区,只写入一次(PerfProbe) |
| 在极少执行的分支中使用闭包 | 功能关闭时仍有分配 | 将捕获的局部变量移入分支 |
| 用 Deep Profile 查找尖峰 | 小函数的耗时被放大 | 在代码中保留标记和计数器,并使用 -perfMarkers |
| 根据 Frame Debugger 调优 GPU | 计时靠猜测 | 使用 metal_trace(Windows 上使用 PIX、Nsight) |
| 在笔记本电脑上固定渲染比例 | 设备冷却时正常,发热或负载时出现卡顿 | 使用动态分辨率;只有像素数上限固定时才以固定比例使用 STP |
| 以为预热能解决首帧卡顿 | 仍然耗时 250 ms | 根据捕获结果判断:JIT 表示 IL2CPP,加载表示异步 |
| 把分散帧任务当作解决方案 | 峰值降低,总耗时不变 | 先测量未分摊的耗时,再修正结构 |
| 在运行时添加 MeshCollider | 生成时出现尖峰,伴随 Physics.BakePhysXCollisionMeshData | 在作业中使用 Physics.BakeMesh,并预烘焙碰撞网格 |
通过 EditorUserBuildSettings 设置开发标记 | 所有平台配置文件都发生改变 | 使用 BuildPlayerOptions 或构建配置文件资源 |
根据 BuildReport 的 totalSize 估算大小 | 178 MB 的应用显示为 1.9 GB | 查看磁盘上的实际交付文件夹 |
将 InstantiateAsync 当作解决卡顿的办法 | 随后某一帧耗时 17 到 83 ms | 使用对象池,并在过渡期间预热 |
| 对新构建版本的首次启动进行基准测试 | 212 与 22 ms 的差异 | 丢弃首次启动的数据 |
未经批准不得更改的内容
游戏手感(固定时间步长、最大允许时间步长、动画时序、输入、AI 更新);未提供前后对比截图时,不得更改美术方向(纹理尺寸、灯光、LOD 阈值、渲染比例);不得更改流水线、引擎版本或重写 ECS;除非游戏运行时分配量为 0 B,且 A/B 测试证明有收益,否则不得关闭增量 GC;不得盲目调高 asyncUploadBufferSize(永远不会恢复原值,控制台电子书第 31 页);发布版本不得关闭 VSync;前后对比之间不得更改基准测试路径。
交接
- 接收: 来自 scenario-unity-expert 的数字化简报;来自 scenario-unity-architecture 和 scenario-unity-gameplay 的系统代码;来自 scenario-unity-rendering-lighting 的流水线资源;来自 scenario-unity-shaders 的着色器;来自 scenario-unity-vfx 的效果;来自 scenario-unity-ui 的 UI;来自 scenario-unity-pipeline-automation 的 AssetBundle 和 Addressables。
- 交付: 已归因的成本、前后对比表、CSV、跟踪记录、PNG:将变体剥离结果交给 scenario-unity-shaders;将灯光、阴影和后期处理裁剪结果交给 scenario-unity-rendering-lighting;将过度绘制预算交给 scenario-unity-vfx 和 scenario-unity-ui;将物理设置交给 scenario-unity-gameplay;将重复项修复、CI 性能测试和构建配置交给 scenario-unity-pipeline-automation;将设备和热验证结果交给 scenario-unity-mobile;将 Web 限制交给 scenario-unity-web。
Unity 6.3 说明
- SRP Batcher + GRD + GPU 遮挡(17.3 URP 资源包含遮挡标志);动态批处理在 6.5 中弃用,在 6.6 中废弃。
- 在 DX12/Vulkan/主机平台上,拆分图形作业为默认设置(Oc6T4hh5gaI [00:03:04]);6.3 URP 模板中的设置为:Windows 开启/拆分、Android 关闭/原生、Metal 关闭/原生;各平台的 getter 为内部接口(观察所得)。
GraphicsStateCollection为实验性功能,仅在开发版播放器中记录跟踪;WebGPU 跟踪功能将在 6.4 中提供。FrameDebuggerUtility为内部接口。- Profile Analyzer 1.4.0、Memory Profiler 1.1.12、Project Auditor 1.0.2(从 6.4 起内置)、Performance Testing 3.5.0(从 6.6 起为核心组件)以及 Profiling Core 1.0.3(
ProfilerCounterValue所需)均为按名称添加的预安装包。 - Mesh LOD 支持 6.2 及更高版本。
Physics.BakeMesh(int, bool)已弃用:请传入mesh.GetEntityId()。6.3 使用 Boehm(非分代式);CoreCLR 尚待后续实现。
参考资料
references/procedures.md:十八项流程,包含完整调用方式、各自的实测和记录结果。references/expert-notes.md:各位专家的原则,包含时间戳、分歧和判定条件。references/critique.md:用于评判性能优化过程的评分标准。references/gui-paths.md:Profiler、Frame Debugger、Rendering Debugger、Project Auditor 和设置路径。references/sources.md:所有来源的凭据、URL、最佳时间戳和修订历史。scripts/ut_perf.py(运行器端),scripts/AgentKit/Performance/*.cs(Editor 作业),scripts/Runtime/Performance/*.cs(PerfProbe、AllocScope)。