一、性能问题不是“感觉有点卡”
60 FPS 的目标意味着每帧只有约 16.67ms:
16.67ms 帧预算├── Game Thread → Gameplay、Actor Tick、复制准备├── Render Thread → 生成渲染命令├── RHI Thread → 提交图形 API 命令└── GPU → 真正执行绘制、计算、后处理但这些不是简单相加。CPU 和 GPU 可能并行工作,真正决定帧率的是当前流水线中最慢的环节:
Game Thread 8ms → Render Thread 12ms → GPU 20ms ↑ GPU 是瓶颈当 GPU 变快后,Game Thread 可能又成为瓶颈。所以“我把某个函数优化了 2ms”只有在它确实位于关键路径上时才有意义。
性能分析的第一步永远是测量瓶颈,而不是猜。
二、先建立帧预算
对一个 60 FPS 的多人合作游戏,可以先设一个粗略预算:
| 区域 | 目标预算 | 主要内容 |
|---|---|---|
| Game Thread | 6–8ms | 回合逻辑、UI 状态、Actor Tick |
| Render Thread | 4–6ms | 渲染命令、场景代理更新 |
| GPU | 10–14ms | 阴影、材质、后处理、UI |
| 网络处理 | 包含在 CPU 内 | 收包、复制、RPC、序列化 |
| 余量 | 2–4ms | 突发事件、平台差异 |
这不是必须遵守的固定数字,目标是让团队有一个共同语言:
“这项功能大概需要 0.3ms Game Thread”比“这个功能应该挺轻的”更适合做工程决策。30 FPS、120 FPS、掌机和移动端会有不同预算。性能目标应该在项目早期声明,而不是上线前才第一次测量。
三、Stat 命令:先做现场体检
UE 的 Stat 命令适合快速回答“哪一类东西在花时间”。常用命令包括:
stat unit // Game、Draw、RHIT、GPU 等帧时间stat fps // 帧率和基础帧时间stat game // Actor Tick、组件和 Gameplay 相关统计stat sceneRenderingstat gpu // GPU Pass 时间(平台支持时)stat slate // Slate / UI 统计stat net // 网络收发、带宽、复制相关数据不同引擎版本、平台和渲染后端显示的字段可能不同,重点是先看量级和趋势,不要把某个统计项的名字当成绝对真相。
1 stat unit 怎么看
Frame: 16.6 msGame: 7.2 msDraw: 10.8 msGPU: 14.4 ms这里的 Frame 受到同步关系影响,不一定等于三者相加。判断瓶颈时,看 Game、Draw、GPU 谁接近或超过自己的预算,再用更细的工具下钻。
四、Unreal Insights:从一帧下钻到函数
Stat 告诉你“哪一层慢”,Unreal Insights 用时间线告诉你“慢在哪里”。它适合分析:
- 长帧和随机 Hitch
- Game Thread 上突然出现的函数尖峰
- 线程之间的等待关系
- 异步加载、GC、Shader 编译造成的卡顿
- 网络收包和复制任务的时间分布
一个常见的分析流程:
1. 复现问题并记录 Trace2. 在 Insights 中找到长帧3. 观察 Game / Render / RHI / GPU 时间线4. 放大长帧,找到最长的 Scope5. 回到代码确认调用路径6. 修复后用相同场景重新记录1 Bookmark 比“看起来卡”有用
TRACE_BOOKMARK(TEXT("MissionFinished"));在回合结束、生成大量物品、切换关卡等关键位置打 Bookmark,之后能在时间线上快速定位业务事件。没有书签时,你只能在一堆匿名的函数调用里猜哪一帧是问题发生的。
2 自定义计时 Scope
TRACE_CPUPROFILER_EVENT_SCOPE(MissionSettlement);
ResolveWinner();ApplyRewards();SaveRoundResult();Scope 名称要表达业务动作,而不是重复函数名。一个好的 Scope 让你可以直接回答“结算到底花在选胜者、发奖励还是存档”。
五、Game Thread 常见瓶颈
1 Tick 数量不是唯一指标
1000 个很轻的 Tick 可能不如一个做了全表扫描的 Tick:
void UQuestSubsystem::Tick(float DeltaSeconds) { for (const FPlayerData& Player : AllPlayers) { for (const FItemData& Item : AllItems) { Evaluate(Player, Item); } }}如果玩家和物品都增长,这段逻辑是 O(P × I)。优化方向可能是:
- 只在物品状态变化时重新计算
- 缓存不变的估价结果
- 把筛选拆成索引或 Tag 集合
- 将不影响本帧结果的工作延迟到任务队列
2 蓝图也需要被测量
蓝图不是天然慢,问题通常是:
- Event Tick 里反复查找对象
- 每帧创建临时数组或字符串
- 嵌套循环扫描所有 Actor
- UI Binding 隐式高频求值
- 大量 Cast 和组件查找
先用 Profiler 找到实际 Scope,再决定是否搬到 C++。把没有瓶颈的蓝图全改成 C++,很可能只增加维护成本。
六、GPU 瓶颈:不要只看 Draw Call 数量
GPU 时间可能来自:
几何处理 → 阴影 → Base Pass → 光照 → 反射 / GI → 后处理 → TSR / TAA → UI常见方向:
| 现象 | 可能原因 |
|---|---|
| 分辨率提高后时间近似按像素增长 | 像素着色器、后处理、带宽 |
| 远处物体增加后变慢 | 几何、阴影、可见性 |
| 只有大量透明特效时变慢 | 半透明 Overdraw |
| 阴影质量提高后明显变慢 | Shadow Map / Virtual Shadow Maps |
| UI 很多但场景不变时变慢 | Slate 绘制、布局失效或过度绘制 |
Draw Call 数量只是线索。一个复杂材质的 Draw Call 可能比多个简单材质更贵;一个透明粒子还可能重复覆盖同一组屏幕像素很多次。
七、Hitch:平均帧率掩盖不了长帧
平均 60 FPS 不代表体验稳定:
大多数帧:16ms偶发一帧:250ms平均值仍然可能接近 60 FPS玩家却明显感觉到卡顿所以要同时看:
- P50:典型帧
- P95 / P99:尾部帧时间
- 最大帧时间:最严重的卡顿
- Hitch 发生时的业务事件
常见 Hitch 来源:
LoadSynchronous()Shader 编译大规模 UObject 创建GC / Cluster 重建同步保存首次创建渲染资源解决思路不是“把一切放到后台线程”。某些 UObject、渲染资源和 Gameplay 状态只能在特定线程或阶段操作。正确做法是使用引擎提供的异步加载、分帧创建、预热和资源生命周期机制。
八、网络性能也要放进同一张图
多人游戏的性能不只是一条 FPS 曲线:
客户端帧率 ↔ Game Thread 收包 / 处理复制 ↔ 发送输入 / RPC ↔ 服务器 Tick / 复制准备 ↔ 带宽、丢包、重传服务器每秒向所有连接检查所有 Actor,会同时消耗 CPU 和带宽。前面讲的 Relevancy、Dormancy、Replication Graph,最终都应该通过测量验证:
优化前:每个客户端 120KB/s,复制准备 4ms优化后:每个客户端 38KB/s,复制准备 1.2ms不要只看客户端“收到了多少字节”,还要看服务器为生成这些字节做了多少工作。
九、一个可重复的性能分析流程
定义目标 → 复现并记录基线 → Stat 确认瓶颈线程 → Insights 定位业务 Scope → 提出一个最小改动 → 在相同场景重新测量 → 记录收益、代价和回归风险一次只验证一个主要假设:
假设:回合结算卡顿来自同步保存实验:只把保存改成异步,其他代码不动结果:长帧从 180ms 降到 32ms结论:继续处理异步保存完成前的生命周期问题如果一次改动同时重写了数据结构、Tick 和 UI,很难知道收益来自哪里,也很难在之后发生回归时定位原因。
十、合作副本的性能检查表
回合开始 → 是否一次性 Spawn 太多 Actor? → 物品图标和预览模型是否同步加载?
战斗阶段 → UI 是否每帧轮询 MissionState? → 每次技能请求是否全量扫描玩家和目标? → 战斗事件是否广播了不必要的数据?
回合结束 → 结算、奖励、统计是否在同一帧完成? → 存档是否阻塞 Game Thread? → 结果 UI 是否触发整棵 Widget Tree 重排?性能不是某个“优化阶段”的独立任务。它和资产加载、复制、消息、UI、存档都有连接,应该在每一篇架构设计里顺手问一句:这个状态变化的频率和拥有者是什么?
十一、总结
| 概念 | 一句话 |
|---|---|
| 帧预算 | 把 16.67ms 或目标平台的预算分给各个系统 |
| Stat | 快速确认 Game、Draw、GPU、网络等大方向 |
| Unreal Insights | 用时间线从长帧下钻到业务 Scope |
| Hitch | 关注 P95/P99 和最大帧时间,而不只是平均 FPS |
| CPU 瓶颈 | 常见于 Tick、全量扫描、同步加载和对象创建 |
| GPU 瓶颈 | 常见于像素、阴影、透明 Overdraw、后处理和带宽 |
性能优化不是找到一个“慢函数”然后结束,而是建立一套可重复的测量—假设—验证循环。 当每一次优化都有基线和证据,性能就不再是玄学,也不会只依赖某个同事的体感。