ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

游戏开发实战:性能优化分类清单与 Unity / Cocos 引擎技巧

游戏开发实战:性能优化分类清单与 Unity / Cocos 引擎技巧

1. 为什么性能优化要"分类"来做

在真实项目里,性能问题从来不是"某个函数慢"这么简单。一个掉帧现象背后,可能是 Draw Call 过多、GC 抖动、物理计算爆炸、纹理内存超限、脚本逻辑空转中的任意一种,甚至是它们叠加的结果。

所以工程上更有效的做法是:先按资源维度分类建立检查清单,再针对具体引擎落地技巧。分类的价值在于:

  • 让排查有顺序,避免"凭感觉改代码";
  • 让团队有统一的性能预算(Budget)语言;
  • 让优化成果可度量、可回归。

下面先给出通用的分类框架,再分别整理 Unity 与 Cocos Creator 的实战技巧。

2. 性能优化分类总览

性能问题

CPU 侧

GPU 侧

内存侧

加载与 IO

网络与逻辑

脚本逻辑 / GC

Draw Call / 批处理

物理与动画

Overdraw / 填充率

Shader 复杂度

后处理与分辨率

纹理 / 网格 / 音频

托管堆与对象池

资源包与首包

场景切换与预加载

协议与同步频率

AI 与寻路

2.1 CPU 侧:脚本逻辑与 GC

  • 避免每帧 new 对象:临时 Vector、List、字符串拼接都是 GC 大户。
  • 减少 Update 数量:成百上千个Update空转是常见隐形开销,能合并就合并,能事件驱动就别轮询。
  • 字符串与装箱:"score:" + score每帧执行会产生大量临时字符串;枚举作为字典 Key 会装箱。
  • 算法复杂度:O(n²) 的碰撞检测、每帧全量排序、每帧Find查找,都是典型雷区。

2.2 CPU 侧:Draw Call 与批处理

  • 合批的前提是材质、贴图、渲染状态一致;
  • 图集(Atlas)是 UI 与 2D 场景的第一优化手段;
  • 静态物体标记 Static,动态小物件考虑 GPU Instancing;
  • UI 层级中穿插不同材质会打断合批,注意节点顺序。

2.3 GPU 侧:Overdraw 与填充率

  • 半透明 UI 层层叠加是移动端最大杀手,全屏 UI 面板叠加 3~4 层就可能吃掉大半带宽;
  • 全屏后处理(Bloom、Blur)在低端机上要提供降级开关;
  • Shader 里避免discard、避免复杂分支、避免逐像素的高开销计算。

2.4 内存侧:资源与托管堆

  • 纹理是内存第一大户,尺寸、压缩格式(ASTC/ETC2)、Mipmap 都要按平台规划;
  • 音频区分长音与短音,长音用流式加载;
  • 托管堆关注 Mono/IL2CPP 堆的峰值与碎片,避免频繁大对象分配。

2.5 加载与 IO

  • 首包与热更包分离,按场景/玩法做分包;
  • 预加载与异步加载结合,避免切场景卡顿;
  • 资源引用计数要能真正释放,防止"加载了但没卸载"。

2.6 网络与逻辑

  • 同步频率按重要性分级(自己/队友/远处单位);
  • 状态同步做增量与压缩,避免全量广播;
  • 寻路与 AI 分帧计算,不要所有单位同一帧跑 A*。

3. Unity 实战技巧

3.1 Profiling 先行

  • Profiler看 CPU 主线程与渲染线程,Frame Debugger看 Draw Call 拆分;
  • Memory Profiler抓快照对比,定位纹理与托管堆泄漏;
  • 真机 Profiling 才是准的,Editor 数据只能做相对参考。

3.2 脚本层

// 反例:每帧产生临时字符串与闭包voidUpdate(){label.text="HP: "+hp+"/"+maxHp;}// 正例:缓存 StringBuilder,值变化时才刷新privatereadonlyStringBuilder_sb=newStringBuilder(32);privateint_lastHp=-1;voidUpdate(){if(hp==_lastHp)return;_lastHp=hp;_sb.Clear();_sb.Append("HP: ").Append(hp).Append('/').Append(maxHp);label.text=_sb.ToString();}
  • 用CompareTag("Player")替代tag == "Player",避免字符串比较;
  • 高频查找用字典缓存,别在Update里GetComponent/Find;
  • 大量同类对象用Job System + Burst或ECS处理纯计算逻辑。

3.3 渲染层

  • SRP Batcher:同 Shader 变体下可显著降低 SetPass Call,注意材质属性要放在 CBUFFER 中;
  • GPU Instancing:大量相同 Mesh 的植被、子弹、小怪首选;
  • Occlusion Culling与LOD Group配合使用,远景降模;
  • UI 用Canvas 拆分:频繁变化的元素单独一个 Canvas,避免整块重建;
  • 关闭不必要的Raycast Target,UI 事件检测也是 CPU 开销。

3.4 资源与内存

  • 纹理按平台设置压缩格式,UI 图集控制在 2048 以内;
  • 使用Addressables做资源加载与释放,配合引用计数;
  • 对象池复用子弹、特效、飘字,避免频繁 Instantiate/Destroy;
  • 音频短音效设为 Decompress On Load,长 BGM 设为 Streaming。

3.5 物理与动画

  • 固定时间步Fixed Timestep不要设得过小;
  • 碰撞体优先用简单形状,Mesh Collider 只给静态关键物体;
  • 动画用Animator 的 Culling Mode关闭不可见角色的更新;
  • 大量骨骼动画考虑 GPU Skinning。

4. Cocos Creator 实战技巧

4.1 渲染与合批

  • 自动合批依赖同材质同贴图,UI 图集要规划好,避免一张图一个材质;
  • 使用多纹理合批(Multi-Texture Batching)减少 Draw Call;
  • 2D 场景注意节点层级,穿插不同材质的节点会打断合批;
  • 慎用Mask / Graphics组件,它们会打断合批并增加额外 Draw Call。

4.2 脚本与 GC

// 反例:每帧创建临时对象update(dt:number){constpos=this.node.position.clone();pos.x+=this.speed*dt;this.node.setPosition(pos);}// 正例:复用临时向量private_tmp=newVec3();update(dt:number){Vec3.copy(this._tmp,this.node.position);this._tmp.x+=this.speed*dt;this.node.setPosition(this._tmp);}
  • 避免在update中做getComponent、find、字符串拼接;
  • 事件监听记得off,否则节点销毁后仍被引用导致泄漏;
  • 大量逻辑用schedule降频,不必每帧执行。

4.3 资源管理

  • 使用Asset Bundle分包,按场景/玩法拆分,控制首包体积;
  • resources.load加载的资源要release,注意引用计数;
  • 纹理压缩按平台选择(Android ETC2、iOS ASTC),避免运行时解压;
  • 预制体(Prefab)实例化用对象池,NodePool是官方提供的现成方案。

4.4 UI 与 2D 优化

  • Label 使用Char Cache / BMFont,长文本频繁刷新是性能黑洞;
  • 减少 Label 的Overflow重排,动态文本尽量用固定尺寸;
  • 滚动列表用虚拟列表,只渲染可视区域内的 Item;
  • 关闭不需要的Widget对齐更新,减少每帧布局计算。

4.5 原生与平台

  • 小游戏平台注意包体与内存上限,微信小游戏对内存尤其敏感;
  • 使用WebGL / 原生时关注首帧加载时间,做启动优化;
  • 真机调试用 Chrome DevTools 或引擎自带 Profiler 看帧耗时分布。

6. 不同优化方案的实际收益量级

下面给的是中低端安卓机(骁龙 6 系 / 天玑 700 级别)上的经验量级,用于判断"值不值得做",不是精确基准。真实数据一定要以自己项目的 Profiler 为准。

优化项典型收益生效前提备注
消除每帧字符串拼接GC Alloc 从每帧几 KB 降到接近 0,卡顿尖峰明显减少该逻辑确实每帧执行收益体现在卡顿而非平均帧率
合并 Update / 事件驱动脚本 Update 总耗时降 20%~50%Update 数量在数百级以上数量少时几乎无感
UI 图集重构 + Canvas 拆分Draw Call 降 30%~60%,UI 重建耗时降一半以上原本图集碎片化严重图文混排场景收益打折
GPU Instancing同屏数百个相同物件时,Draw Call 从数百降到个位数材质/网格完全一致需要支持 Instancing 的 Shader
SRP BatcherSetPass Call 降 50% 以上同 Shader 变体、属性在 CBUFFER对 Draw Call 数量本身无影响
关闭多余 Raycast TargetUI 事件检测 CPU 耗时降 10%~30%界面上有大量非交互 Graphic改动成本极低,性价比高
纹理压缩格式修正纹理内存降 50%~75%原本用了未压缩或 RGBA32需注意画质与加载耗时
对象池替代 Instantiate瞬时卡顿基本消除,峰值帧耗时降 30%+高频生成销毁的物件首次创建仍有开销
降低 Overdraw(去全屏遮罩)GPU 填充率降 20%~40%,低端机帧率提升明显半透明层叠 ≥ 3 层移动端收益远大于 PC
后处理降级开关低端机帧率提升 10~20 FPS原本开了 Bloom/Blur表现损失需美术确认
物理碰撞体简化物理耗时降 30%~70%大量 Mesh Collider手感风险高,需谨慎
虚拟列表长列表滚动帧率从 20 提升到稳定 60列表项 > 50实现成本中等

6.1 怎么读这张表

  • 收益分两类:一类降平均耗时(如 Draw Call、物理),一类降卡顿尖峰(如 GC、Instantiate)。后者对玩家体感影响更大,优先级往往更高;
  • 性价比排序:改动小、风险低的先做——关闭 Raycast Target、纹理压缩、对象池,这三项通常一周内能落地且几乎无副作用;
  • 收益打折是常态:表里的数字是"理想条件"下的上限,图文混排、历史包袱、美术要求都会让它打折,实际能拿到 50%~70% 就已经不错。

6.2 一个真实的优化顺序示例

假设一个中低端机上稳定 25 FPS 的 2D 卡牌项目,按收益/成本排序大致是:

  1. 先看 Profiler:确认瓶颈在 CPU 还是 GPU;
  2. 若是 GC 尖峰 → 对象池 + 消除字符串拼接,通常能消掉大部分卡顿;
  3. 若是 Draw Call 高 → 图集整理 + Canvas 拆分,先动高频界面;
  4. 若是 GPU 填充率 → 去全屏半透明遮罩、后处理降级;
  5. 最后才考虑架构级重构(合并 Update、换渲染管线),因为风险最高。

5. 现实约束:理论上能改,实际上改不动

前面列的都是"正确的事",但真实项目里,很多优化点在纸面上成立、在工程上却动不了。这一节专门讲这些"知道但做不到"的地方,避免读者照着清单硬改反而踩坑。

5.1 美术表现与合批的天然冲突

最典型的就是图文混排。理论上,把 UI 里所有图片和文字排到同一张图集、同一材质下,Draw Call 能压到极低;但实际项目里:

  • 策划要求"标题文字压在插画上、插画再叠一层描边",这种层叠关系本身就决定了渲染顺序,顺序一乱合批就断;
  • 每个活动页面的美术资源是外包分批交付的,图集规划往往在需求变更中被反复打散;
  • 文字用 BMFont 还是系统字体、图片是否带透明通道,都会让"同材质"这个前提失效。

结论:图文交替导致的 Draw Call 增加,多数情况下是需求成本而不是技术成本。真正能做的不是"消灭它",而是:

  1. 把静态背景层和动态内容层拆成两个 Canvas / 两个渲染层,让静态部分只重建一次;
  2. 给 UI 定一个Draw Call 预算(例如单个全屏界面 ≤ 30),超了才去谈图集重构;
  3. 优先优化高频出现的界面(主界面、战斗 HUD),低频活动页不值得为几个 Draw Call 大动干戈。

5.2 架构与历史包袱

  • 老项目的 Update 满天飞:理论上合并 Update 收益很大,但一个跑了三年的项目,脚本分散在几十个预制体上,改动面覆盖全部玩法,回归测试成本远超收益;
  • 第三方 SDK 与插件:很多性能热点在不可修改的 SDK 内部(广告、统计、支付),只能通过"延迟初始化""降低调用频率"在外围缓解;
  • 引擎版本锁定:想用新版本的合批优化或 Job System,但项目锁在旧版本,升级带来的兼容风险大于性能收益。

结论:这类问题要用外围手段解决——降频、延迟、分帧、开关,而不是改内部实现。

5.3 优化收益与风险的权衡

有些优化"能改",但风险大于收益:

  • 把Mesh Collider全换成Box Collider,物理开销确实降了,但碰撞手感变了,玩家能感知到"卡进墙里";
  • 把动画帧率从 30 降到 15,CPU 省了,但动作表现明显变糙;
  • 把纹理从 2048 降到 1024,内存省了一半,但 UI 在高端机上肉眼可见地糊。

结论:性能优化永远是和表现、手感、工期做三方博弈。判断标准应该是"这个优化玩家能不能感知到",感知不到的才值得做。

5. 通用优化原则与落地建议

  1. 先测量再优化:没有 Profiler 数据的优化都是猜测;
  2. 设定性能预算:如"中端机稳定 30 FPS、Draw Call < 150、内存 < 300MB";
  3. 建立回归机制:把关键场景做成性能测试用例,每次版本对比;
  4. 分级降级:画质档位、特效开关、分辨率缩放,给低端机留后路;
  5. 优化要可回滚:每次只改一个变量,记录数据变化。

7. 总结

性能优化不是一次性任务,而是贯穿项目生命周期的工程习惯。分类清单帮你"不漏项",引擎技巧帮你"改得准",Profiler 数据帮你"改得对",而现实约束和收益量级帮你判断"哪些值得改"。把这几件事做成团队流程,掉帧问题才会从"玄学"变成"可解"。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进