Unity GPU 合批优化详解
Unity GPU 合批优化详解
从 Draw Call 到 Static Batching、Dynamic Batching、GPU Instancing、SRP Batcher,再到 Profiler 和 Frame Debugger 验证。
这份文档的目标不是让你背概念,而是让你能看懂 Unity 为什么卡、该用哪种优化、怎么证明优化真的有效。
先说结论
Unity 渲染优化里经常听到一句话:
减少 Draw Call。
但真正准确的说法应该是:
减少 CPU 给 GPU 下命令的成本,同时不要把 GPU、内存、显存、剔除效率搞坏。
也就是说,Draw Call 少不一定就快,Draw Call 多也不一定就慢。你要先知道瓶颈在 CPU 还是 GPU,然后选对方法。
本文重点补充 3 个最容易被忽略的点:
- 底层原理:CPU、Render Thread、GPU、Draw Call、Batch、SetPass 到底是什么关系。
- Unity 实操:Static Batching、Dynamic Batching、GPU Instancing、SRP Batcher 分别适合什么,怎么开,什么时候不要用。
- 性能验证:怎么用 Stats、Profiler、Frame Debugger 判断合批有没有成功,以及优化后是不是真的变快。
1. 一帧画面是怎么出来的
玩家看到一帧画面,大概经历这条链路:
游戏逻辑 ↓ CPU 更新世界状态 ↓ CPU 准备渲染数据 ↓ Render Thread / 渲染系统组织绘制命令 ↓ CPU 把命令提交给图形 API / 驱动 ↓ GPU 执行绘制 ↓ 屏幕显示1.1 CPU 做什么
CPU 更像游戏的“调度员”。
它负责:
- 玩家输入。
- 角色移动。
- AI。
- 物理。
- 动画状态。
- UI 逻辑。
- 场景物体显隐。
- 告诉 GPU 这一帧要画什么。
比如你的 RPG 场景里有:
玩家 敌人 NPC 房子 地面 树 UI 小地图CPU 要先判断这些东西当前在哪里、是否可见、用什么材质、用什么 Shader,然后再组织绘制命令。
1.2 GPU 做什么
GPU 更像“绘图工厂”。
它负责:
- 顶点变换。
- 光照计算。
- 阴影。
- 贴图采样。
- 像素填充。
- 后处理。
- 最终输出画面。
如果 GPU 很忙,通常会看到:
GPU Usage 高 GPU Frame Time 高 降低分辨率以后 FPS 明显提升 关闭阴影/后处理以后 FPS 明显提升如果 CPU 很忙,通常会看到:
CPU Main Thread 高 Render Thread 高 Batches 很多 SetPass Calls 很多 场景物体很多但 GPU 并不满2. Draw Call、Batch、SetPass 是什么
2.1 Draw Call
Draw Call 是:
CPU 让 GPU 绘制一次几何体的命令。
比如:
画这棵树 画这个箱子 画这个敌人 画这个按钮每发一次绘制命令,都有 CPU 端开销。
开销包括:
- 检查渲染状态。
- 切换 Mesh。
- 切换 Material。
- 切换 Shader。
- 上传参数。
- 设置贴图。
- 组织命令。
所以 Draw Call 多了,CPU 会忙。
2.2 Batch
Batch 可以理解成:
Unity 最终提交给底层渲染的一批绘制任务。
很多时候你会把 Batch 和 Draw Call 近似理解成同一类指标。
Unity Game 窗口右上角Stats里常看到:
Batches: 1200 SetPass calls: 300 Tris: 1.5M Verts: 2.2M这里的Batches越多,通常 CPU 渲染提交压力越大。
但它不是唯一指标。
例如:
Batches: 20 Tris: 20MDraw Call 很低,但是三角面太多,GPU 也可能很卡。
2.3 SetPass
SetPass 可以理解成:
切换一次 Shader Pass / 材质渲染状态。
它通常比普通 Draw Call 更贵。
比如:
物体 A 用 URP Lit 材质 物体 B 用 Unlit 材质 物体 C 用另外一个带透明的 ShaderUnity 可能要不断切换渲染状态。
这种切换会让 CPU 和渲染线程变忙。
所以优化时不要只看 Batches,也要看:
SetPass Calls Render Thread Camera.Render DrawRenderers3. 为什么合批能优化性能
合批的本质:
把多个能一起画的物体,尽量变成更少的绘制提交。
3.1 不合批
假设场景里有 1000 个石头:
CPU: 画石头 1 CPU: 画石头 2 CPU: 画石头 3 ... CPU: 画石头 1000可能产生:
1000 Batches3.2 合批以后
如果这些石头能合批:
CPU: 这一批石头一起画可能变成:
10 BatchesCPU 提交命令次数减少,Render Thread 压力下降。
3.3 一个快递例子
不合批:
1000 个包裹 跑 1000 趟快递站合批:
1000 个包裹装进几个大箱子 跑几趟就送完但注意:
如果你为了合批,把所有东西塞进一个超级大箱子,搬不动了,也会慢。
对应 Unity 里就是:
Draw Call 降了 但是 Mesh 太大 显存变多 剔除变差 GPU 更忙所以优化不是盲目合批,而是选合适的批。
4. Unity 能不能合批,看哪些条件
Unity 合批时最关心这些东西:
4.1 Mesh
Mesh 是模型数据。
包括:
顶点 法线 切线 UV 颜色 骨骼权重 三角面索引不同合批方式对 Mesh 要求不同。
例如:
- GPU Instancing 通常要求 Mesh 相同。
- Static Batching 不要求 Mesh 完全相同,但要求静态且材质兼容。
- Dynamic Batching 对 Mesh 顶点数量限制很严格。
4.2 Material
材质是合批最容易踩坑的地方。
看起来一样,不代表能合。
比如:
Tree_Mat Tree_Mat可以尝试合批。
但如果你复制了材质:
Tree_Mat_01 Tree_Mat_02哪怕两个材质参数完全一样,Unity 也可能认为它们是两个不同材质,导致不能合批。
4.3 Shader
Shader 不一样,通常不能合。
例如:
URP/Lit URP/Unlit这两个渲染流程不同,不能随便合在一起。
4.4 Texture
贴图不一样,也经常破坏合批。
例如 100 个 UI 图标:
icon_1.png icon_2.png icon_3.png如果每张图都独立成材质,Batch 会很多。
更好的做法是:
把小图打进同一个 Atlas 用同一个 UI 材质4.5 Transform
位置、旋转、缩放也会影响合批。
尤其 Dynamic Batching。
比如非统一缩放:
scale = (1, 2, 1)可能导致动态合批失败。
4.6 Lightmap
静态场景常用 Lightmap。
但如果物体使用不同 Lightmap,或者 Lightmap 参数不同,也可能被分成不同批次。
例如:
房子 A 使用 Lightmap 0 房子 B 使用 Lightmap 1它们可能不能放进同一个批次。
4.7 Render Queue 和透明排序
透明物体很麻烦。
因为透明物体通常需要按照距离从远到近排序。
例如:
玻璃 水面 半透明特效这些东西不能像不透明物体那样随便重排顺序,所以合批空间会小很多。
5. Static Batching 静态合批
5.1 一句话理解
把不会动的场景物体提前整理到一起,减少运行时绘制提交。
适合:
房子 墙 地面 道路 石头 固定树木 固定栏杆 固定装饰物不适合:
玩家 敌人 NPC 会移动的平台 会旋转的机关 会动态隐藏显示很多次的物体5.2 Unity 怎么开启
项目设置:
Edit → Project Settings → Player → Other Settings → Static Batching场景物体:
选中 GameObject → Inspector → Static 勾选常用是勾:
Batching Static如果你直接勾Static总开关,可能还会包含:
Lightmap Static Navigation Static Occluder Static Occludee Static这些不是每个物体都应该全开。
5.3 底层大概做了什么
原来:
House Mesh Rock Mesh Fence Mesh Road Mesh静态合批后,Unity 会把这些静态物体的顶点数据整理成更大的批次。
它的核心思想是:
既然这些东西不会动 那就提前把它们的世界坐标算好 运行时少做事5.4 优点
优点很直接:
减少 CPU 渲染提交压力 适合大场景固定物体 不需要每帧重新合并5.5 缺点
最大的缺点是内存。
假设你有 100 个路灯,原本都共享同一个 Mesh:
Lamp Mesh x 1 100 个 Transform静态合批可能会把它们的顶点按世界空间复制整理。
结果:
内存增加 显存增加 加载时间增加所以 Static Batching 不是无脑全开。
5.6 RPG 场景例子
你的MainCity这类城市场景,可以这样分:
适合 Static Batching:
城墙 地砖 房屋主体 固定木箱 固定摊位 固定灯笼架 石阶 桥 栏杆不适合 Static Batching:
玩家 Player Enemy NPC 任务交互点高亮 可破坏木箱 会开关的门 会摇摆的旗帜 粒子特效5.7 常见错误
错误 1:
把 Player 勾成 Static后果:
移动、光照、剔除、导航都可能出问题错误 2:
整个场景所有东西全 Static后果:
内存上涨 加载慢 动态物体异常错误 3:
材质乱七八糟,每个房子一个材质后果:
勾了 Static,但合批效果很差更好的做法:
同类建筑尽量共用材质 贴图尽量做 Atlas6. Dynamic Batching 动态合批
6.1 一句话理解
Unity 每帧在 CPU 上临时把一些小 Mesh 拼起来画。
适合:
很小的低模道具 简单小物件 少量小特效模型不适合:
角色 复杂敌人 高模建筑 大场景物体 大量动态单位6.2 为什么它会消耗 CPU
Dynamic Batching 不是免费优化。
它每帧要做:
检查哪些物体能合 把顶点从本地空间转换到世界空间 重新整理顶点数据 提交给 GPU如果模型很小,这点 CPU 开销可能值得。
如果模型稍微复杂一点,就会变成:
为了减少 Draw Call CPU 反而更累6.3 顶点限制
常见说法是:
Dynamic Batching 适合 300 顶点以下的小 Mesh更准确一点:
它和顶点属性数量有关顶点属性包括:
Position Normal UV Tangent Color属性越多,能动态合批的顶点数越少。
例如:
一个只有 Position + UV 的小面片 可能容易合 一个带 Normal + Tangent + 多套 UV 的模型 顶点不多也可能失败6.4 例子
可以考虑 Dynamic Batching:
金币:80 顶点 小石子:60 顶点 小草片:40 顶点不建议:
玩家:8000 顶点 敌人:6000 顶点 建筑:20000 顶点6.5 什么时候关掉也没问题
如果你项目里:
URP + SRP Batcher 大量中高模 大量角色 GPU Instancing 已经处理重复物体Dynamic Batching 的价值可能不高。
甚至可能因为 CPU 每帧处理合批导致更慢。
实际项目里要用 Profiler 测,不要凭感觉。
7. GPU Instancing
7.1 一句话理解
同一个 Mesh、同一个 Material 的大量物体,让 GPU 自己复制很多份。
适合:
大量同种树 大量草 大量石头 大量子弹 大量同款士兵 大量同款敌人不适合:
每个物体 Mesh 都不同 每个物体 Material 都不同 普通 SkinnedMeshRenderer 角色 需要复杂独立材质逻辑的单位7.2 传统方式
1000 棵树:
CPU: 画树 1 CPU: 画树 2 CPU: 画树 3 ... CPU: 画树 10007.3 Instancing 方式
CPU 只告诉 GPU:
Mesh = Tree Material = Tree_Mat Instances = [ position 1, position 2, position 3, ... ]GPU 自己在 Vertex Shader 里读取每个实例的位置、旋转、缩放,然后画出来。
7.4 Unity 怎么开启
材质上:
选中 Material → Inspector → Enable GPU Instancing代码里:
material.enableInstancing=true;7.5 不要用 renderer.material 乱改材质
这是新手最常踩的坑。
错误写法:
renderer.material.color=Color.red;renderer.material会在运行时实例化一份材质。
原本:
100 个敌人共用 Enemy_Mat变成:
Enemy_Mat_Instance_1 Enemy_Mat_Instance_2 Enemy_Mat_Instance_3 ...结果:
材质不再相同 合批被破坏 内存也增加更推荐:
usingUnityEngine;publicsealedclassEnemyColorSetter:MonoBehaviour{privatestaticreadonlyintBaseColorId=Shader.PropertyToID("_BaseColor");[SerializeField]privateRenderertargetRenderer;privateMaterialPropertyBlockpropertyBlock;privatevoidAwake(){propertyBlock=newMaterialPropertyBlock();}publicvoidSetColor(Colorcolor){targetRenderer.GetPropertyBlock(propertyBlock);propertyBlock.SetColor(BaseColorId,color);targetRenderer.SetPropertyBlock(propertyBlock);}}这样可以避免复制材质。
注意:
MaterialPropertyBlock 对 GPU Instancing 很常用 但在某些 SRP Batcher 场景下可能让 SRP Batcher 失效 最终要用 Frame Debugger 验证7.6 DrawMeshInstanced 例子
如果你想自己批量画一堆相同 Mesh,可以用:
usingSystem.Collections.Generic;usingUnityEngine;publicsealedclassSimpleInstancedRocks:MonoBehaviour{[SerializeField]privateMeshrockMesh;[SerializeField]privateMaterialrockMaterial;[SerializeField]privateintcount=500;[SerializeField]privateVector2area=newVector2(50f,50f);privatereadonlyList<Matrix4x4>matrices=newList<Matrix4x4>(1023);privatevoidStart(){rockMaterial.enableInstancing=true;for(inti=0;i<count;i++){floatx=Random.Range(-area.x,area.x);floatz=Random.Range(-area.y,area.y);Vector3position=newVector3(x,0f,z);Quaternionrotation=Quaternion.Euler(0f,Random.Range(0f,360f),0f);Vector3scale=Vector3.one*Random.Range(0.8f,1.2f);matrices.Add(Matrix4x4.TRS(position,rotation,scale));}}privatevoidUpdate(){constintmaxInstanceCount=1023;for(inti=0;i<matrices.Count;i+=maxInstanceCount){intbatchCount=Mathf.Min(maxInstanceCount,matrices.Count-i);Graphics.DrawMeshInstanced(rockMesh,0,rockMaterial,matrices.GetRange(i,batchCount));}}}上面这个例子好懂,但GetRange每帧会产生 GC。
更好的写法是提前准备数组:
usingUnityEngine;publicsealedclassNoGcInstancedRocks:MonoBehaviour{privateconstintMaxInstanceCount=1023;[SerializeField]privateMeshrockMesh;[SerializeField]privateMaterialrockMaterial;[SerializeField]privateintcount=500;[SerializeField]privateVector2area=newVector2(50f,50f);privateMatrix4x4[]matrices;privatevoidAwake(){count=Mathf.Min(count,MaxInstanceCount);matrices=newMatrix4x4[count];rockMaterial.enableInstancing=true;for(inti=0;i<count;i++){Vector3position=newVector3(Random.Range(-area.x,area.x),0f,Random.Range(-area.y,area.y));Quaternionrotation=Quaternion.Euler(0f,Random.Range(0f,360f),0f);Vector3scale=Vector3.one*Random.Range(0.8f,1.2f);matrices[i]=Matrix4x4.TRS(position,rotation,scale);}}privatevoidUpdate(){Graphics.DrawMeshInstanced(rockMesh,0,rockMaterial,matrices,count);}}实际大型项目里,如果实例数量特别多,会继续考虑:
DrawMeshInstancedIndirect GPU Culling ComputeBuffer DOTS / Entities Graphics但初学阶段先理解 GPU Instancing 就够了。
7.7 RPG 项目例子
假设你后面场景里有:
100 个同款小怪 300 个同款资源点 2000 株草 500 棵同款树选择建议:
同款草、树、石头 → GPU Instancing 敌人普通 SkinnedMeshRenderer → 先别强行 Instancing 敌人武器/掉落物等静态 Mesh → 可以考虑 Instancing角色类对象更复杂,因为通常涉及:
骨骼动画 蒙皮 装备 材质变化 血条 状态特效不要一上来就为了 Instancing 把角色系统搞复杂。
先保证:
敌人数量合理 Animator 不滥用 LOD 做好 Update 控制好 对象池做好8. SRP Batcher
8.1 一句话理解
在 URP/HDRP 里,减少材质和 Shader 参数切换的 CPU 成本。
它和 GPU Instancing 不是同一个东西。
GPU Instancing 解决:
大量相同 Mesh + 相同 Material 的绘制提交问题SRP Batcher 解决:
大量使用兼容 Shader 的不同物体,切换材质参数时 CPU 太忙的问题8.2 适合什么
适合:
URP 项目 大量不同 Mesh 大量不同 Material Shader 兼容 SRP Batcher 场景中物体较多例如 RPG 城市场景:
房子 A 房子 B 墙 石头 道路 木箱 摊位 灯笼这些东西 Mesh 不一样,不能靠 GPU Instancing 全解决。
但如果 Shader 兼容 SRP Batcher,CPU 设置材质参数的成本会明显下降。
8.3 怎么开启
URP 项目一般在 URP Asset 中开启:
Project 窗口找到 UniversalRenderPipelineAsset → Inspector → Advanced → SRP Batcher不同 Unity 版本位置可能略有差异,找不到就搜索:
SRP Batcher8.4 Shader 怎么兼容
一般 Unity 官方 URP Lit、URP Unlit 都是兼容的。
自定义 Shader 要注意:
材质属性要放在 UnityPerMaterial CBUFFER 里 不要乱写每材质常量缓冲如果 Shader 不兼容,Frame Debugger 里可能会看到 SRP Batcher 没有生效。
8.5 SRP Batcher 和 MaterialPropertyBlock
很多项目会用 MaterialPropertyBlock 给不同物体设置颜色。
比如:
敌人受击变红 NPC 头顶高亮 任务目标描边这种写法很方便,但要注意:
MaterialPropertyBlock 可能让该 Renderer 走不了 SRP Batcher不是说不能用,而是:
少量使用没问题 大量使用要验证如果你有 5000 个草要变色,更适合考虑 GPU Instancing 的实例属性。
如果只是玩家锁定的 1 个 NPC 高亮,用 MaterialPropertyBlock 很正常。
9. 四种合批方式对比
| 技术 | 解决什么 | 适合 | 不适合 | 主要风险 |
|---|---|---|---|---|
| Static Batching | 静态物体减少绘制提交 | 建筑、地面、固定道具 | 会移动的物体 | 内存、显存增加 |
| Dynamic Batching | 小 Mesh 每帧临时合并 | 低顶点小物件 | 角色、大模型、大量复杂物体 | CPU 额外开销 |
| GPU Instancing | 大量相同物体 | 草、树、石头、同款道具 | Mesh/Material 各不相同的物体 | 材质实例化会破坏 |
| SRP Batcher | 降低 SRP 材质切换成本 | URP/HDRP 大量物体 | Built-in 管线、不兼容 Shader | 自定义 Shader 不兼容 |
9.1 选择口诀
固定不动的大场景:
Static Batching大量重复的同款物体:
GPU InstancingURP 项目里很多不同物体:
SRP Batcher很小很简单的动态物体:
Dynamic BatchingUI:
Canvas 拆分 + 图集 + 减少材质切换10. UI 合批也很重要
很多人只看 3D 场景,忽略 UI。
但 Unity UI 也会产生 Batch。
10.1 UI 为什么会打断合批
UI 合批受这些影响:
不同 Canvas 不同材质 不同贴图 不同 Mask 不同字体材质 不同渲染顺序 频繁 Layout 重建10.2 常见 UI 错误
错误 1:
所有 UI 都放一个巨大 Canvas后果:
聊天消息刷新一下 整个 Canvas 可能都要重建错误 2:
每个图标都是独立贴图后果:
UI Batches 增加 材质/贴图切换增加错误 3:
大量使用 Mask、Shadow、Outline后果:
额外绘制和材质变化10.3 RPG 项目 UI 拆分建议
例如你的游戏里有:
MainUI GameUI ChatUI ToastUI SettingsUI可以按变化频率拆:
静态 HUD Canvas: 玩家头像 名字 固定按钮 动态 HUD Canvas: 血条 蓝条 小地图玩家点 弹窗 Canvas: 设置 聊天 登录 Toast Canvas: 提示飘字这样一个小地图点移动,不会导致整个主界面跟着大重建。
10.4 聊天界面例子
聊天 UI 里常见问题:
消息一多 滚动列表卡 LayoutGroup 每帧重排 ContentSizeFitter 频繁计算 文字生成 Mesh优化建议:
消息 Item 用对象池 只刷新可见范围 减少 ContentSizeFitter 嵌套 文字高度缓存 新消息来了再刷新布局 不要每帧 ForceUpdateCanvasesCanvas.ForceUpdateCanvases()不是不能用,但不要每帧用。
它适合:
打开界面时需要立刻拿到布局尺寸 特殊一次性布局计算不适合:
Update 每帧调用 滚动时每帧调用 聊天刷新时无脑调用多次11. 怎么判断合批成功
11.1 Game 窗口 Stats
打开:
Game 窗口 → Stats重点看:
Batches SetPass calls Tris Verts例子:
优化前:
Batches: 1800 SetPass calls: 650 Tris: 1.2M优化后:
Batches: 350 SetPass calls: 120 Tris: 1.2M这说明 CPU 渲染提交压力可能下降了。
但还要看 Profiler,不要只看 Stats。
11.2 Profiler 看 CPU 还是 GPU
打开:
Window → Analysis → Profiler先看:
CPU Usage Rendering GPU Usage Memory UI如果 CPU Rendering 高:
重点看 Batches、SetPass、Render Thread如果 GPU 高:
重点看阴影、后处理、分辨率、Overdraw、三角面、贴图采样11.3 Frame Debugger
打开:
Window → Analysis → Frame Debugger点击 Enable。
你可以一条一条看 Unity 这一帧画了什么。
没合批时可能看到:
Draw Mesh Tree Draw Mesh Tree Draw Mesh Tree Draw Mesh Tree合批成功可能看到:
Draw Mesh Tree x 100 Render Mesh (instanced) SRP BatchFrame Debugger 最重要的作用是:
告诉你为什么没合批。
常见原因:
Different Material Different Shader Keywords Different Lightmap Different Render Queue Different Shadow Pass Object is not static Material property block11.4 正确测试流程
不要一边改一边凭感觉。
建议流程:
1. 记录优化前数据 2. 只改一个优化点 3. 进入同一个场景同一个视角 4. 记录优化后数据 5. 对比 CPU、GPU、Batches、SetPass、内存 6. 确认没有画面错误记录表可以这样写:
| 项目 | 优化前 | 优化后 | 是否变好 |
|---|---|---|---|
| FPS | 42 | 58 | 是 |
| Batches | 1600 | 420 | 是 |
| SetPass | 500 | 130 | 是 |
| CPU Main Thread | 18ms | 11ms | 是 |
| Render Thread | 12ms | 5ms | 是 |
| GPU Time | 9ms | 9ms | 基本不变 |
| Memory | 1.2GB | 1.35GB | 变高,需要接受或继续优化 |
12. 实战案例 1:MainCity 场景
假设你的MainCity有:
房子 80 个 地砖 300 块 摊位 40 个 树 200 棵 灯笼 120 个 箱子 100 个 玩家 1 个 敌人 20 个 NPC 30 个12.1 错误做法
每个房子一个材质 每个树一个材质 所有东西都勾 Static 玩家也 Static 敌人也 Static UI 全放一个 Canvas可能结果:
Batches 很高 内存也高 动态物体异常 UI 刷新卡12.2 推荐做法
建筑:
共享材质 勾 Batching Static 考虑 Lightmap地面:
合并贴图或使用统一材质 静态 必要时分块,避免一个超级大 Mesh树:
同款树多 → GPU Instancing 固定少量树 → Static Batching 也可以 风吹动画树 → 不要直接 Static玩家、敌人、NPC:
不 Static 使用对象池 Animator 控制数量 LOD 远距离降低更新频率UI:
GameUI、ChatUI、ToastUI 分 Canvas 小地图点只在数据变化时刷新 聊天列表用回收/对象池12.3 预期效果
优化前:
Batches: 2500 SetPass calls: 700 CPU Main Thread: 22ms Render Thread: 15ms优化后:
Batches: 500 SetPass calls: 160 CPU Main Thread: 13ms Render Thread: 6ms这类提升主要来自:
材质复用 Static Batching GPU Instancing UI Canvas 拆分13. 实战案例 2:5000 棵树
13.1 错误做法
5000 个 GameObject 每个树都 Instantiate 每个树 renderer.material 单独改颜色 每棵树一个材质实例结果:
Batches 高 内存高 CPU 高 可能还有 GC13.2 改进做法
同一个 Tree Mesh 同一个 Tree Material Material 开 Enable GPU Instancing 颜色差异用实例属性 远处树用 LOD 或 Impostor如果树完全不动,也可以对一部分使用 Static Batching。
但如果树有风动画:
不要直接把会动的树干/树叶 Static可以考虑:
GPU Instancing + Shader 风动画14. 实战案例 3:敌人和战斗
假设你后面做单机战斗:
EnemyPoint 生成 20 个 Enemy Enemy 有 Animator Enemy 会巡逻 Enemy 会攻击 Enemy 有血条不要第一步就追求敌人 GPU Instancing。
优先级应该是:
1. 敌人对象池 2. Animator 参数不要每帧乱 Set 3. AI 逻辑分帧/降频 4. 远距离敌人降低更新频率 5. 血条 UI 只在变化时刷新 6. 同款武器/掉落物/特效再考虑 Instancing为什么?
因为角色性能常见瓶颈不只在 Draw Call。
还可能在:
Animator SkinnedMeshRenderer 骨骼数量 AI Update 寻路 碰撞检测 UI 血条 特效所以敌人战斗优化不能只盯着合批。
15. 常见误区
15.1 误区:Draw Call 越少越好
不绝对。
如果你把整个城市合成一个超级大 Mesh:
Batches 降了但可能:
视锥剔除变差 看不见的东西也被画 内存暴涨 加载变慢正确做法:
按区域、材质、可见性合理分块15.2 误区:勾了 Static 就一定合批
不一定。
还要看:
材质 Shader Lightmap Renderer 状态 是否真的 Static15.3 误区:物体长一样就能合批
不一定。
两个物体看起来一样,但如果:
材质不是同一个引用 Shader Keyword 不同 贴图不是同一个 Lightmap 不同 Render Queue 不同都可能不能合。
15.4 误区:GPU Instancing 能解决所有重复物体
不一定。
GPU Instancing 喜欢:
同 Mesh 同 Material 差异用实例数据表达如果每个物体都有完全不同材质、不同 Shader、不同动画,它就不适合。
15.5 误区:Dynamic Batching 一定是优化
不一定。
Dynamic Batching 会消耗 CPU。
如果你的瓶颈本来就在 CPU,它可能让情况更糟。
16. 项目优化检查清单
16.1 场景物体
检查:
固定建筑是否 Static 动态物体是否误 Static 同类物体是否共用材质 贴图是否可以 Atlas 远处物体是否有 LOD 看不见的区域是否能被剔除16.2 材质
检查:
有没有大量 xxx_Instance 材质 有没有代码使用 renderer.material 是否能改为 sharedMaterial 或 MaterialPropertyBlock Shader Keyword 是否过多 透明材质是否太多16.3 UI
检查:
Canvas 是否按变化频率拆分 聊天列表是否对象池 小地图是否每帧无脑刷新 是否每帧 ForceUpdateCanvases 图片是否使用图集 字体材质是否统一 Mask 是否过多16.4 战斗角色
检查:
敌人是否对象池 Animator 参数是否变化时才 Set AI 是否每帧全量计算 血条是否只在血量变化时刷新 远距离敌人是否降频 特效是否对象池16.5 验证
每次优化后记录:
FPS CPU Main Thread Render Thread GPU Time Batches SetPass calls Tris Verts GC Alloc Memory17. 最后总结
可以用这张表记住:
| 场景 | 优先方案 |
|---|---|
| 固定建筑、道路、墙 | Static Batching |
| 大量同款树、草、石头 | GPU Instancing |
| URP 下大量不同物体 | SRP Batcher |
| 很小的低模动态物体 | Dynamic Batching |
| 聊天、背包、HUD | Canvas 拆分 + 图集 + 对象池 |
| 玩家、敌人、NPC | 对象池 + LOD + 动画/AI 降频 |
一句话:
合批不是目的,稳定帧率才是目的。
真正的优化流程应该是:
发现卡 ↓ Profiler 判断 CPU 还是 GPU ↓ Frame Debugger 看渲染提交 ↓ 只改一个优化点 ↓ 重新测试 ↓ 记录数据这样做,你就不会靠感觉优化,也不会把项目越改越乱。
