1. 项目概述为什么动态合批是Unity性能优化的双刃剑在Unity项目开发中尤其是面向移动端或WebGL平台时性能优化是每个开发者都绕不开的坎。当你打开Profiler看到那密密麻麻的Draw Call峰值时血压可能也跟着上来了。动态合批Dynamic Batching作为Unity内置的、旨在自动减少Draw Call的优化技术听起来像是“开箱即用”的救星。但实际情况是很多开发者只是简单地在Player Settings里勾选了“Dynamic Batching”然后就把它抛在脑后结果可能适得其反性能不升反降。我经历过不止一个项目在低端安卓机上明明开启了动态合批帧率却卡顿得厉害。用Profiler深挖才发现CPU耗时被一个叫“DynamicBatchRenderer”的项占了大头GPU反而在“等饭吃”。这就是对动态合批理解不透彻的典型后果。它并非一个“勾选即优化”的魔法开关而是一套有严格适用条件和潜在副作用的机制。理解它才能驾驭它。简单来说动态合批的核心目标是将多个使用相同材质、顶点数较少且正在运动的网格Mesh物体在CPU端将它们的世界坐标顶点数据合并然后一次性提交给GPU渲染从而将多个Draw Call合并为一个。这听起来很美但代价是合并计算本身需要CPU时间。在现代高端GPU上一个Draw Call的开销可能远小于CPU进行顶点变换和合并的计算开销。因此动态合批是一把典型的双刃剑用得好是神兵利器用不好反而自损性能。本文将基于我多年的实战经验带你彻底拆解Unity动态合批。我们不止看官方手册怎么说更要深入它生效的底层逻辑、剖析各种限制条件的真实影响并给出在不同场景如移动端、小游戏、复杂场景下的具体优化策略和实操步骤。目标是让你不仅能看懂Profiler里关于Batching的数据更能主动地设计和调整你的场景、模型和材质让动态合批真正为你的项目帧率服务。2. 动态合批的核心原理与限制条件深度解析要优化先得懂原理。盲目应用只会带来更多问题。Unity的动态合批主要处理两类对象静态网格和动态生成的几何体如粒子、线渲染器。它们的批处理逻辑有本质区别我们主要讨论更常见也更容易出问题的网格动态合批。2.1 核心工作原理CPU端的顶点“搬家”动态合批针对网格的核心动作发生在CPU上。对于每一帧Unity会检查哪些移动的GameObject符合批处理条件。如果符合它会进行以下操作顶点变换将这些物体的模型空间Local Space顶点数据逐个乘以它们的变换矩阵Transform Matrix转换到世界空间World Space。注意这个步骤是每个顶点、每帧都要进行的。数据合并将所有转换到世界空间的顶点数据包括位置、法线、UV等拼接到一个大的顶点缓冲区Vertex Buffer中。单次提交将这个合并后的大缓冲区连同共享的材质信息一次性通过一个Draw Call提交给GPU。关键点在于第一步顶点变换从GPU转移到了CPU。在正常的渲染流程中顶点变换是由GPU的顶点着色器Vertex Shader并行完成的效率极高。而CPU是串行处理大量顶点的矩阵乘法计算会非常沉重。这就是为什么动态合批有时会导致CPU瓶颈。2.2 硬性限制条件为什么我的物体没被合批官方手册列出了限制但我们需要结合实战理解其影响顶点属性数量限制900个与顶点数限制225个 这是最容易误解的一点。限制不是“网格顶点数少于225”而是“顶点数 * 每个顶点的属性数量 900”。一个顶点属性包括位置float3、法线float3、切线float4、UV0float2、UV1float2、颜色float4等。举例1一个使用标准Lit着色器的网格通常包含位置、法线、切线、UV0共3342 12个浮点数即12个属性。那么最大可批处理顶点数为900 / 12 ≈ 75。这意味着即使你的模型只有100个面约200个三角面顶点数可能超过150也可能因为超过75个顶点而无法合批。举例2一个简单的Unlit纹理着色器可能只使用位置和UV0共5个属性。那么可批处理顶点数可达900 / 5 180。实战检查在编辑器中选中模型文件在Inspector的Model页签下查看“Vertex Count”。更准确的是编写简单代码在运行时输出Mesh.vertexCount和着色器所需的顶点属性。材质实例必须完全相同 这是最常导致合批失败的原因。动态合批要求物体共享同一个材质实例Material Instance而不仅仅是同一个材质资产Material Asset。踩坑场景你在场景中放置了10个相同的预制体Prefab每个预制体上的Renderer都引用了项目中的同一个“Stone.mat”材质资产。运行时如果你通过脚本修改了其中任何一个物体的材质属性如material.color或material.SetFloatUnity会为该Renderer创建一个此材质资产的新实例。修改后这10个物体就使用了10个不同的材质实例动态合批立即失效。正确做法如果需要批量修改使用相同材质物体的属性应使用MaterialPropertyBlock。它允许你修改Renderer的着色器属性而不会创建新的材质实例从而保持合批有效。光照贴图Lightmap的影响 使用了光照贴图的物体其渲染器会包含额外的索引和偏移信息用于采样光照贴图。只有当多个物体不仅使用相同材质实例而且它们的光照贴图数据指向光照贴图图集的位置也完全相同时才能被动态合批。这在实际烘焙光照的场景中很难满足因此对动态物体使用光照贴图会严重阻碍合批。多Pass着色器与正向渲染附加光多Pass着色器例如一些老旧的透明或效果着色器每帧会执行多个渲染通道Pass。动态合批通常只能合批第一个Pass后续Pass会打断合批。正向渲染每像素光在Unity的Forward Rendering路径下除了一个主方向光Base Pass外每个额外的每像素光Per-pixel Light都会为受其影响的物体产生一个额外的渲染Pass。动态合批只能作用于Base Pass这些额外的Additive Pass无法被合批会导致Draw Call激增。这是场景中放置多个实时光源时性能骤降的主要原因之一。注意动态合批对阴影投射器Shadow Casters的处理比较特殊。即使物体材质实例不同只要在渲染阴影时所需的材质属性如Alpha Test的阈值相同Unity仍然可以合批它们的阴影绘制。但这不影响物体本身在相机视图中的渲染合批。3. 实战优化策略从场景设计到运行时控制理解了原理和限制我们就可以制定针对性的优化策略。优化不是事后补救而应该贯穿于项目制作的始终。3.1 模型与资产准备阶段严格控制动态物体的面数对于需要频繁移动且希望被合批的物体如金币、子弹、小道具其网格顶点数必须严格控制。目标是将单个网格的顶点数控制在合批上限以内通常建议低于150为复杂着色器留出余地。在建模软件中或导入Unity后使用LOD Group或简单的网格简化工具进行处理。简化着色器与顶点数据为动态小物体定制简单的着色器。如果不需要法线贴图就不要使用需要切线数据的标准着色器如果不需要光照就使用Unlit着色器。减少每个顶点的属性数量可以直接提升可合批的顶点上限。使用纹理图集Texture Atlas这是解决“材质实例相同”问题的根本方法之一。将多个小物体如不同颜色的药水、不同花纹的砖块的纹理合并到一张大图上。这样所有物体都可以使用同一个材质和同一张纹理通过UV坐标来区分不同部分完美满足动态合批条件。避免对动态物体使用光照贴图计划中需要移动的物体不要标记为Static也不要为其生成光照贴图。考虑使用Light Probe光照探针来为动态物体提供间接光照这对性能更友好。3.2 场景构建与运行时脚本策略善用MaterialPropertyBlock 这是动态修改物体外观而不破坏合批的唯一推荐方法。下面是一个对比示例// 错误做法破坏合批 void ChangeColorBad(MeshRenderer renderer, Color color) { renderer.material.color color; // 这会创建新的材质实例 } // 正确做法使用MaterialPropertyBlock保持合批 void ChangeColorGood(MeshRenderer renderer, Color color) { MaterialPropertyBlock propBlock new MaterialPropertyBlock(); renderer.GetPropertyBlock(propBlock); // 获取现有属性如果有 propBlock.SetColor(_Color, color); // 设置颜色属性 renderer.SetPropertyBlock(propBlock); // 应用属性块 }你可以用MaterialPropertyBlock修改颜色、浮点数、纹理偏移等几乎所有着色器属性。管理实时光源数量 在正向渲染路径下严格控制每个物体受到的每像素光数量。在Quality Settings中降低“Pixel Light Count”的值例如从4改为2。对于不重要的光源将其模式改为“Baked”或“Not Important”Realtime模式下的Vertex/SH模式这样它们就不会产生额外的渲染Pass。分而治之的渲染层管理 对于大量同质小物体如一片草地如果因为顶点数超限或其它原因无法动态合批可以考虑使用GPU Instancing。这是一个比动态合批更现代、更高效的批处理技术尤其适用于大量完全相同的网格和材质。在材质的Inspector中勾选“Enable GPU Instancing”即可。注意它要求Unity版本支持和着色器兼容。动态合批的开关策略 不要全局武断地开启或关闭动态合批。更精细的策略是针对平台在PC/主机等GPU强大的平台可以考虑关闭动态合批Player Settings - Dynamic Batching以避免不必要的CPU开销。在低端移动设备上则开启它。针对场景你可以通过脚本在运行时控制GraphicsSettings.useScriptableRenderPipelineBatching false;并结合自定义渲染管线逻辑进行更精细的控制对于URP/HDRP项目。3.3 性能分析与验证方法优化离不开数据佐证。你必须学会使用Unity Profiler来验证动态合批的效果。打开Profiler进入Rendering区域关注Batches和Saved by batching这两个关键指标。Saved by batching直观地显示了通过静态/动态合批节省了多少个Draw Call。如果这个数字很高说明合批效果显著。如果很低甚至为0说明你的场景几乎没有从合批中受益。使用Frame Debugger帧调试器 这是分析Draw Call和合批情况的终极工具。Window - Analysis - Frame Debugger。点击Enable游戏会暂停并显示当前帧的所有渲染事件。滚动列表你会看到类似Draw Dynamic (合批)和Draw Dynamic的条目。前者代表一次合批的绘制包含了多个物体后者代表单独的绘制。点击任何一个绘制事件Scene视图会高亮显示这次调用绘制了哪些物体。你可以清晰地看到哪些物体被合批在一起哪些没有以及为什么没有例如材质不同、着色器不同Pass等。自定义性能标记 在代码中你可以使用Profiler.BeginSample和Profiler.EndSample来标记特定批处理逻辑的CPU耗时更精确地定位动态合批带来的CPU开销是否值得。4. 常见问题排查与进阶技巧实录在实际项目中你会遇到各种各样合批失效的“灵异事件”。这里记录一些我踩过的坑和解决方案。4.1 为什么开启了动态合批但“Saved by batching”还是0这是最常见的问题。请按照以下清单逐一排查排查项可能原因验证方法/解决方案顶点数超限网格顶点属性总和超过900。检查模型顶点数使用更简单的着色器。材质实例不同脚本直接修改了renderer.material。全部改用MaterialPropertyBlock。Shader不支持使用了多Pass复杂Shader。使用Frame Debugger查看绘制事件确认是否被拆分为多个Pass。考虑简化或更换Shader。物体为Static静态物体由静态合批处理不计入动态合批节省。正常现象。检查静态合批是否生效Saved by batching应仍有值。使用了Skinned Mesh Renderer蒙皮网格渲染器不支持动态合批。这是Unity的限制。对于大量相同角色考虑使用GPU Instancing需支持蒙皮的Shader或第三方插件。缩放值包含负值物体的Transform缩放含有负值如-1, 1, 1。负值缩放会导致镜像Unity无法安全地将其与其他物体合批。避免使用负值缩放通过建模或Shader实现镜像效果。4.2 移动端尤其是Android上的特殊考量移动平台GPU架构多样Mali, Adreno, PowerVRCPU相对较弱因此策略需要调整优先使用纹理图集和GPU Instancing在支持Vulkan或Metal的现代移动设备上GPU Instancing的效率远高于动态合批。应作为首选方案。谨慎评估动态合批的收益在中低端安卓设备上CPU往往是瓶颈。你需要用Profiler连接真机仔细对比开启和关闭动态合批时的CPU渲染线程耗时(RenderThread)和总Batches数。如果开启后CPU耗时增加而Batches减少不明显则应关闭动态合批。注意GLES2/3的差异如果项目需要支持老旧的GLES2.0设备动态合批可能是减少Draw Call的有效手段因为GLES2的Draw Call开销相对较大。但同样需要权衡CPU开销。4.3 粒子系统Particle System的合批粒子系统属于“动态生成的几何体”其合批逻辑与网格不同且通常自动进行效率很高。但需要注意发射器停止后的粒子停止发射后存活的粒子如果材质相同通常仍然可以被合批。不同材质的粒子使用不同材质的粒子系统无法合批。尽量将粒子特效的纹理合并到图集中使用同一个材质。Trail Renderer和Line Renderer同理尽量让它们共享材质。4.4 与URP/HDRP的兼容性在可编程渲染管线SRP如URP和HDRP中动态合批的支持情况有所不同URP默认支持动态合批且其实现可能比内置管线更高效。URP还提供了强大的SRP Batcher。SRP Batcher是一个更高级的批处理系统它通过缓存着色器属性大幅降低使用不同材质但相同Shader的物体的Draw Call准备开销。SRP Batcher的优先级高于动态合批。在URP中优化思路应优先保证Shader符合SRP Batcher要求使用CBUFFER其次再考虑动态合批。HDRP不支持传统的网格动态合批因为其渲染复杂度极高CPU端合批的代价通常超过收益。HDRP主要依赖GPU Instancing和其自身的渲染管线优化。最后我的个人体会是动态合批更像是一个“遗产”优化特性它在特定历史时期和特定硬件条件下非常有效。在现代游戏开发中我们的武器库应该更丰富纹理图集、GPU Instancing、SRP Batcher是更优先考虑的方案。动态合批应当作为一个补充手段在针对低端设备进行优化时经过严谨的性能分析后再决定是否启用。永远记住没有银弹数据Profiler和具体场景Frame Debugger才是你做优化决策的唯一依据。当你对屏幕上一片摇曳的小草进行优化时不妨问问自己它们真的需要每帧都移动吗能不能用Shader动画实现能不能用更少的Draw Call画出来多问几个为什么优化的思路自然就打开了。