开场:把游戏想象成一座剧场
先把游戏运行的过程翻译成一场演出:
| 游戏中的概念 | 剧场里的角色 |
|---|---|
| 一帧画面 | 一幕戏,必须在规定时间内演完并"拉开幕布"给观众看 |
| 帧率目标 | 演出节拍:60 FPS 意味着每幕只有16.6 毫秒,30 FPS 是 33.3 毫秒 |
| CPU 主线程 | 导演兼舞台监督,决定这一幕谁上场、做什么(脚本、动画、物理、UI 逻辑) |
| 渲染线程 | 把导演的指令整理成工单,交给美工组 |
| Job Worker 线程 | 临时雇来的帮手,分担搬运和计算(Burst/Job System、部分物理与动画) |
| GPU | 舞台美工和灯光组,真正把布景画出来、把灯打亮 |
| 内存 | 道具仓库 |
| GC(垃圾回收) | 清洁工。平时不管,垃圾堆多了会突然叫停演出来打扫 |
| Draw Call / SetPass | 导演对美工喊的一次"开工!"。每喊一次都有沟通成本,换一套颜料(材质/Shader)成本更高 |
观众抱怨"卡",可能是导演想得太多(CPU 瓶颈),可能是美工画得太慢(GPU 瓶颈),可能是清洁工突然清场(GC 尖峰),也可能是仓库塞满了导致搬运困难(内存问题)。
性能分析工具的作用,就是在剧场里装满摄像头和仪器,找出到底是谁拖慢了演出。
一、Unity Profiler:中央监控室
Window > Analysis > Profiler,这是整个剧场的监控大屏。
上半部分是一排滚动的心电图(各个 Profiler Module),每一列代表一帧:
- CPU Usage:导演和各工种的忙碌程度(最常看)
- GPU Usage:美工组的耗时(部分平台和图形 API 不支持)
- Rendering:今天喊了多少次"开工"(Batches、SetPass Calls)、画了多少三角形
- Memory:仓库里的道具总量和 GC 分配
- Physics / Audio / UI / Video:各个专项部门的状况
- Highlights(2022+):直接告诉你这一幕是导演慢(CPU Bound)还是美工慢(GPU Bound)
心电图上突然出现的尖峰,就是"这一幕演砸了"。点击它,下方就会展开这一帧的详细记录。
两种查看方式:录像回放 vs 部门账本
Timeline 视图:按时间顺序的录像回放
能看到这一幕里,主线程、渲染线程、各个 Worker 线程在同一时间各自在干什么。它特别适合发现"谁在等谁":
- 主线程长时间停在
Gfx.WaitForPresent/Gfx.WaitForGfxCommandsFromMainThread等等待项上,往往说明美工组(GPU)或渲染线程太慢,导演只能干等。 - Worker 线程大面积空白,而主线程挤满任务,说明帮手雇了却没活干,可以考虑用 Job System 分摊工作。
Hierarchy 视图:按部门统计的账本
把这一帧的所有调用按层级汇总,可以按耗时排序:
| 列名 | 含义 |
|---|---|
| Total | 这个部门连同下属一共花了多少时间 |
| Self | 部门负责人自己干活的时间(不含下属) |
| Calls | 被调用了几次 |
| GC Alloc | 这一帧制造了多少垃圾 |
GC Alloc列非常重要。每帧都在制造垃圾的函数,迟早会让清洁工冲上台清场,造成一次明显卡顿。常见的"垃圾制造者"包括:在Update里拼接字符串、频繁new列表、LINQ、装箱、GetComponents返回数组等。
Deep Profile:给每个演员都装追踪器
默认情况下,Profiler 只记录 Unity 内置的关键环节和你脚本的Update这类入口。打开Deep Profile后,每一个 C# 函数调用都会被记录。
代价是:每个演员都背着沉重的追踪器表演,整场演出会明显变慢,测出来的绝对数值会失真,大项目甚至会卡到无法操作。它适合用来定位"是谁",不适合用来测量"有多慢"。
自定义标记:给演员挂名牌
更轻量、更精确的做法是只给你怀疑的演员挂上名牌:
usingUnity.Profiling;publicclassEnemyManager:MonoBehaviour{staticreadonlyProfilerMarkers_UpdateAI=newProfilerMarker("Enemy.UpdateAI");staticreadonlyProfilerMarkers_PathFind=newProfilerMarker("Enemy.PathFind");voidUpdate(){using(s_UpdateAI.Auto()){foreach(vareinenemies){e.Think();using(s_PathFind.Auto()){e.FindPath();}}}}}这样在 Profiler 里就能直接看到Enemy.UpdateAI和Enemy.PathFind各花了多少时间,开销几乎可以忽略,还可以保留在正式版本中(非 Development Build 时会被剔除)。
二、Frame Debugger:定格慢放,逐笔拆解布景
Window > Analysis > Frame Debugger
如果说 Profiler 告诉你"美工组这一幕花了很久",那 Frame Debugger 就是把这一幕定格,然后一笔一笔回放美工的绘制过程。
你可以看到:
- 这一帧总共有多少个绘制事件,以及它们的先后顺序
- 每一笔画的是哪个物体、用了哪个 Shader、哪个 Pass、什么参数和贴图
- 画到第 N 笔时,舞台是什么样子
- 为什么这两次绘制没能合批(Unity 会直接写出原因,比如"材质不同"、“使用了不同的光照贴图”、“物体受不同的光照影响”)
它尤其适合解决这类问题:
- “UI 明明很简单,为什么有几十个 Draw Call?”
- “这个物体为什么画了两遍?”(多余的 Pass、额外的阴影投射)
- “后处理到底加了几层?”
三、Memory Profiler:仓库盘点与 CT 扫描
Profiler 里的 Memory 模块只能看到仓库大致有多满。真正的仓库盘点需要安装独立的Memory Profiler包(Package Manager 中搜索)。
它的工作方式是拍快照:在某一时刻,把仓库里的每一件道具都登记下来。
- Summary:总览内存被谁占用(托管堆、原生对象、图形资源、未被追踪的部分)
- Unity Objects / All Objects:按类型列出所有对象,比如贴图占了多少、Mesh 占了多少,单个大户一目了然
- 引用链:某个对象为什么还没被释放,是谁抓着它不放
最有用的玩法是对比两张快照:
- 进入关卡前拍一张;
- 进入关卡再退出,回到同一位置后再拍一张;
- 比较差异。
本该被清走的道具还留在仓库里,就是内存泄漏。常见原因包括:静态变量持有引用、事件注册后没有注销、已销毁对象仍被 C# 引用(“空壳对象”,== null返回 true,但托管对象仍占内存)、资源加载后没有卸载。
另一个高频发现是贴图过大:一张 4096×4096 的 UI 背景、没有压缩的贴图、意外开启了 Read/Write,都会在快照里非常显眼。
四、Profile Analyzer:多场演出的统计报告
单看一帧可能是偶然。Profile Analyzer(也是独立包)可以分析几百帧的数据,给出统计结果:
- 某个函数的平均值、中位数、最大值和分布
- 两次录制的对比:优化前 vs 优化后,或者低端机 vs 高端机
它回答的是这类问题:“我改了寻路算法后,到底是真变快了,还是这次运气好?”
就像体检后拿到的化验单,会把各项指标和正常范围、上次结果并排列出来。
五、其他随手可用的小仪器
| 工具 | 比喻 | 用途 |
|---|---|---|
| Game 视图的 Stats 面板 | 门口的体温计 | 快速查看 FPS、Batches、三角形数量。粗略参考,不够精确 |
| Scene 视图的 Overdraw 模式(内置管线) | 透视镜 | 看哪里被重复涂了很多层(大量半透明粒子、UI 叠加) |
| Physics Debugger | 查看隐形的护栏 | 碰撞体是否过多、过于复杂 |
| Rendering Debugger(URP/HDRP) | 灯光检查表 | 查看光照、材质、各种缓冲区 |
| Project Auditor | 建筑安检 | 静态检查代码和资源设置中的常见性能隐患 |
六、外请专科医生:平台级工具
Unity Profiler 主要看"Unity 认识的部分"。当问题深入到 GPU 内部或操作系统层面时,需要请平台的专科医生:
| 工具 | 适用平台 | 擅长 |
|---|---|---|
| RenderDoc | PC、Android 等 | 截取一帧,深入分析每个 Draw Call 的着色器、资源、管线状态 |
| Xcode Instruments / GPU Frame Capture | iOS、macOS | CPU 采样、内存分配、Metal GPU 分析、发热与功耗 |
| Android Studio Profiler | Android | CPU、内存、系统层面的问题 |
| Snapdragon Profiler / Mali Graphics Debugger / Android GPU Inspector | 对应芯片的手机 | 移动 GPU 的带宽、着色器负载 |
| PIX / Nsight | Windows、Xbox、NVIDIA 显卡 | 深度 GPU 分析 |
| 主机平台官方工具 | PS / Xbox / Switch | 各平台的专用分析器 |
移动端还有一个 Unity Profiler 不容易看到的问题:发热降频。手机前几分钟运行流畅,之后因为温度上升而降频,帧率随之下降。需要长时间测试,并结合平台工具观察温度和频率。
七、最重要的原则:去真实剧场检测
在 Editor 里测出来的数据,只能当作排练厅的参考。
Editor 本身也在消耗资源(Scene 视图、Inspector 刷新、编辑器脚本),而且 Editor 中的代码路径和真机不同。正确的做法是:
- 勾选Development Build和Autoconnect Profiler(需要时再勾 Deep Profiling Support);
- 打包到目标设备,尤其是你支持的最低端机型;
- 通过 USB 或局域网,让 Editor 中的 Profiler 连接到设备进行录制。
几个容易踩的坑:
- Editor 中的
EditorLoop不是游戏的开销,分析时不要被它误导。 - 垂直同步会掩盖真实耗时:开启 VSync 时,
WaitForTargetFPS一类的等待会把帧时间"补满"。看到它很长,说明还有余量,而不是有问题。 - 看尖峰,也看平均值:平均 12ms 但偶尔 80ms,玩家感受到的是卡顿;稳定在 20ms,玩家感受到的是不流畅。两者的优化方向不同。
- Debug 日志也有开销:大量
Debug.Log本身就是性能问题,测试前留意一下。
八、一次完整的"看诊"流程
观众反馈"卡" │ ▼ ① 量体温:Stats / Profiler 总览,确认帧时间和尖峰 │ ▼ ② 分科室:Highlights 或 Timeline,判断是 CPU 还是 GPU 瓶颈 │ ├─ CPU 瓶颈 ──> Hierarchy 排序 Self / Total │ ├─ 脚本逻辑太重 → 优化算法、分帧处理、Job System │ ├─ GC Alloc 尖峰 → 对象池、避免每帧分配 │ ├─ 物理太重 → 简化碰撞体、调整 Fixed Timestep │ └─ 渲染准备太重 → Frame Debugger 查合批 │ ├─ GPU 瓶颈 ──> Frame Debugger / RenderDoc │ ├─ 过度绘制 → 减少半透明叠加 │ ├─ 着色器太复杂 → 简化 Shader、降低后处理 │ └─ 分辨率与带宽 → 降低渲染分辨率、压缩贴图 │ └─ 内存问题 ──> Memory Profiler 快照对比 ├─ 泄漏 → 查引用链 └─ 单项过大 → 压缩、降低尺寸、按需加载 │ ▼ ③ 开药:针对性修改 │ ▼ ④ 复查:Profile Analyzer 对比优化前后,确认真的有效结语
把这些工具放回剧场的比喻里:
- Profiler是监控室,告诉你哪一幕出了事、哪个部门最忙;
- Frame Debugger是慢放镜头,告诉你舞台布景是怎么一笔笔画出来的;
- Memory Profiler是仓库盘点,告诉你什么东西该走却没走;
- Profile Analyzer是化验单,告诉你治疗到底有没有效果;
- 平台工具是专科医生,处理 Unity 看不到的深层问题。
性能优化最忌讳凭感觉开药。很多人一上来就合并网格、改 Shader,最后发现真正的瓶颈是某个脚本每帧都在分配内存。先测量、再定位、后修改、再验证,这是这套"体检仪"最重要的价值。