4.4 性能优化 (Performance Optimization) (Profiler, Draw Call, Batching) 4.4 性能优化 (Performance Optimization) 在Unity3D游戏开发中,性能优化是至关重要的环节。一个流畅、高效的游戏体验是吸引玩家并留住玩家的关键。性能不佳的游戏,即使拥有精美的画面和有趣的内容,也可能因为卡顿、掉帧等问题而让玩家感到沮丧。因此,作为开发者,我们需要深入理解性能优化的原理和方法,确保我们的游戏能够在各种设备上稳定运行。 4.4.1 Profiler:性能分析的利器 什么是 Profiler? Profiler 是 Unity 内置的强大的性能分析工具。
在Unity3D游戏开发中,性能优化是至关重要的环节。一个流畅、高效的游戏体验是吸引玩家并留住玩家的关键。性能不佳的游戏,即使拥有精美的画面和有趣的内容,也可能因为卡顿、掉帧等问题而让玩家感到沮丧。因此,作为开发者,我们需要深入理解性能优化的原理和方法,确保我们的游戏能够在各种设备上稳定运行。
什么是 Profiler?
Profiler 是 Unity 内置的强大的性能分析工具。它可以实时监控并记录游戏运行时的各种性能数据,包括 CPU 使用率、GPU 使用率、内存占用、渲染数据、音频数据等等。通过 Profiler,我们可以清晰地了解游戏性能瓶颈所在,从而有针对性地进行优化。
Profiler 的重要性
精准定位性能瓶颈: Profiler 能够详细展示各个模块的性能消耗,例如脚本、渲染、物理、动画等,帮助我们快速找到性能瓶颈。
量化优化效果: 在进行优化后,可以通过 Profiler 再次分析,量化优化效果,确保优化措施真正有效。
持续监控性能: 在开发过程中,可以持续使用 Profiler 监控性能变化,及时发现并解决潜在的性能问题。
跨平台性能分析: Profiler 支持在 Editor 编辑器和真机设备上进行性能分析,帮助我们了解游戏在不同平台上的表现。
如何使用 Profiler?
打开 Profiler 窗口: 在 Unity 编辑器菜单栏中,选择 Window -> Profiler,即可打开 Profiler 窗口。
连接 Profiler:
Editor 编辑器: 默认情况下,Profiler 会自动连接到 Editor 编辑器。
真机设备:
USB 连接: 确保设备已连接到电脑,并在 Build Settings 中选择 "Development Build" 和 "Script Debugging"。在 Profiler 窗口的 "Active Profiler" 下拉菜单中选择你的设备。
IP 地址连接: 在 Build Settings 中勾选 "Development Build" 和 "Script Debugging",并在 "Scripting Backend" 中选择 "Mono" 或 "IL2CPP"。 运行游戏后,在 Profiler 窗口的 "Active Profiler" 下拉菜单中选择 "Enter IP...",输入设备的 IP 地址和端口 (通常是 5555)。
Profiler 窗口界面:
Profiler 窗口主要分为两个区域:
模块选择区域 (Modules): 位于窗口左侧,可以选择要监控的性能模块,例如 CPU Usage, Rendering, Memory, Audio, Physics 等。
图表显示区域 (Charts): 位于窗口右侧,以图表的形式实时显示各个模块的性能数据。
常用的 Profiler 模块:
CPU Usage: 显示 CPU 各个线程的运行情况,包括 Main Thread (主线程)、Rendering Thread (渲染线程)、Job System (作业系统) 等。可以查看脚本、渲染、物理、动画等模块的 CPU 消耗。
Rendering: 显示渲染相关的性能数据,例如 Draw Calls, Batches, SetPass Calls, Triangles, Vertices 等。这是优化 Draw Call 和 Batching 的关键模块。
Memory: 显示内存使用情况,包括 Total Allocated Memory (总分配内存), Mono Heap (Mono 堆内存), Gfx Driver Memory (图形驱动内存) 等。可以帮助我们分析内存泄漏和内存优化问题。
Audio: 显示音频相关的性能数据,例如 CPU Usage, Voices, Clips 等。
Physics: 显示物理相关的性能数据,例如 CPU Usage, кол-во коллайдеров (Collider Count), кол-во динамических коллайдеров (Dynamic Collider Count) 等。
录制 Profiler 数据:
点击 Profiler 窗口左上角的 "Record" 按钮 (或快捷键 Ctrl + P) 开始录制性能数据。再次点击 "Record" 按钮停止录制。录制的数据可以保存为 .data 文件,方便后续分析。
Profiler 代码实践:
我们可以在代码中使用 UnityEngine.Profiling.Profiler 类来标记自定义的 Profiler 区域,以便更精细地分析代码的性能消耗。
using UnityEngine; using UnityEngine.Profiling; public class ProfilerExample : MonoBehaviour { void Update() { // 标记 Profiler 区域 "MyFunction" Profiler.BeginSample("MyFunction"); MyFunction(); Profiler.EndSample(); // 标记 Profiler 区域 "ExpensiveOperation" Profiler.BeginSample("ExpensiveOperation"); ExpensiveOperation(); Profiler.EndSample(); } void MyFunction() { // 一些代码逻辑 Debug.Log("MyFunction is running"); } void ExpensiveOperation() { // 模拟一个耗时操作 for (int i = 0; i < 100000; i++) { Mathf.Sqrt(i); } } }
运行游戏并在 Profiler 窗口中查看 CPU Usage 模块,你将看到 "MyFunction" 和 "ExpensiveOperation" 两个自定义的 Profiler 区域,可以清晰地看到它们各自的 CPU 消耗。
Profiler 工作流程图 (Mermaid):
内容详解:
Profiler 是性能优化的第一步,它帮助我们从宏观到微观地了解游戏性能状况。熟练使用 Profiler,能够让我们告别盲目猜测,基于数据进行优化,从而更高效地提升游戏性能。
什么是 Draw Call?
Draw Call (绘制调用) 是 CPU 向 GPU 发出的渲染指令。每一次 Draw Call 都代表 CPU 通知 GPU 渲染一个物体。CPU 需要准备渲染数据 (例如顶点数据、纹理、材质等),并将其传递给 GPU,GPU 接收到指令后进行实际的渲染工作。
Draw Call 的性能消耗
Draw Call 本身会带来 CPU 开销,主要包括:
状态切换 (State Changes): 每次 Draw Call 都可能需要 GPU 切换渲染状态,例如切换材质、纹理、Shader 等。状态切换会消耗 GPU 时间,尤其频繁的状态切换会成为性能瓶颈。
CPU-GPU 通信开销: CPU 需要将渲染数据传输给 GPU,数据传输本身也需要时间。
GPU 渲染队列管理: GPU 需要管理渲染队列,处理 Draw Call 的调度和执行。
为什么 Draw Call 会成为性能瓶颈?
在现代 GPU 的架构下,GPU 的渲染能力通常很强大,但 CPU 的性能往往相对较弱。过多的 Draw Call 会导致 CPU 成为瓶颈,CPU 忙于处理大量的 Draw Call 指令,无法高效地利用 GPU 的渲染能力,从而导致帧率下降。
影响 Draw Call 数量的因素:
物体数量: 场景中可见物体的数量越多,Draw Call 数量通常也越多。
材质数量: 即使是相同的物体,如果使用了不同的材质,也会增加 Draw Call 数量。
网格 (Mesh) 分割: 过于细分的网格会增加 Draw Call 数量。
Shader 复杂度: 复杂的 Shader 可能导致渲染状态切换更加频繁,间接增加 Draw Call 的性能开销。
阴影 (Shadows): 阴影渲染也会增加 Draw Call 数量。
透明物体 (Transparent Objects): 透明物体的渲染通常需要单独处理,也会增加 Draw Call 数量。
如何查看 Draw Call 数量?
在 Unity Editor 的 Stats 窗口 (Window -> Rendering -> Frame Debugger 或 Game 视图的 Stats 按钮) 中,可以查看当前的 Draw Calls, Batches, SetPass Calls 等渲染统计信息。在 Profiler 的 Rendering 模块中,也可以查看 Draw Calls 和 Batches 的详细数据。
Draw Call 工作流程图 (Mermaid):
内容详解:
理解 Draw Call 的概念及其性能影响,是进行渲染优化的基础。减少 Draw Call 数量是提升渲染性能的关键策略之一。接下来我们将介绍 Batching 技术,它是减少 Draw Call 的有效手段。
什么是 Batching?
Batching (批处理) 是一种将多个小的 Draw Call 合并成一个大的 Draw Call 的技术。通过 Batching,可以减少 CPU 向 GPU 发送 Draw Call 指令的次数,从而降低 CPU 开销,提升渲染性能。
Batching 的类型:
Unity 提供了多种 Batching 技术,主要包括:
Static Batching (静态批处理):
Dynamic Batching (动态批处理):
GPU Instancing (GPU 实例化):
SRP Batcher (可编程渲染管线批处理): (更高级,适用于 Scriptable Render Pipeline)
4.4.3.1 Static Batching (静态批处理)
原理:
Static Batching 将场景中 静态 的物体 (Static Flag 标记为 Static 的物体) 合并成一个大的网格 (Mesh),然后进行一次 Draw Call 渲染。静态物体是指在场景中位置、旋转、缩放等属性不会发生变化的物体。
适用场景:
场景中存在大量静态物体,例如建筑、树木、石头等。
物体共享相同的材质。
优点:
显著减少 Draw Call 数量,提升性能。
对 CPU 开销降低明显。
缺点:
内存占用增加: 静态批处理会将合并后的网格数据存储在内存中,可能会增加内存占用。
不适用于动态物体: 静态批处理只适用于静态物体,动态物体无法参与静态批处理。
场景更新代价高: 如果场景中静态物体发生变化 (例如移动、旋转),需要重新进行静态批处理,代价较高。
合并限制: Unity 对静态批处理的合并数量有限制,如果场景中静态物体数量过多,可能无法全部合并。
代码实践 (静态批处理):
标记物体为 Static: 在 Inspector 窗口中,选中要参与静态批处理的物体,勾选右上角的 "Static" 复选框。
确保物体使用相同的材质 (可选但强烈建议): 为了获得最佳的静态批处理效果,建议静态物体使用相同的材质。如果使用不同的材质,静态批处理仍然会生效,但可能会根据材质进行多次 Draw Call。
静态批处理示例场景:
创建一个简单的场景,包含多个相同的 Cube 物体,并将它们标记为 "Static",并赋予相同的材质。运行游戏,在 Stats 窗口或 Profiler 中观察 Draw Calls 数量,你会发现 Draw Calls 数量显著降低。
4.4.3.2 Dynamic Batching (动态批处理)
原理:
Dynamic Batching 将场景中 动态 的物体 (非 Static Flag 标记的物体) 合并成批次进行渲染。它在每一帧的渲染循环中动态地检测可以合并的物体,并进行批处理。
适用场景:
场景中存在大量 小型 的动态物体,例如小草、小石头、粒子系统中的粒子等。
物体共享相同的材质。
优点:
减少 Draw Call 数量,提升性能。
适用于动态物体,可以处理场景中运动的物体。
自动进行,无需手动设置。
缺点:
限制条件多: Dynamic Batching 对物体的 顶点数量、材质、Shader 等有严格的限制。
顶点数量限制: Unity 官方文档指出,Dynamic Batching 仅适用于顶点数量 小于 900 (Uniform Scale) 或 小于 300 (Non-Uniform Scale) 的网格。超出限制的物体将无法参与动态批处理。
材质限制: 必须使用 相同的材质实例 (Material Instance)。如果使用不同的材质实例,即使材质内容相同,也无法进行动态批处理。
Shader 限制: 不支持使用顶点位置修改的 Shader (例如顶点动画 Shader)。
其他限制: 不支持阴影投射、不支持接收阴影、不支持多 Pass Shader 等。
性能提升有限: 由于限制条件较多,Dynamic Batching 的性能提升效果相对 Static Batching 较弱。
代码实践 (动态批处理):
new Material(Shader) 或 Material.Instantiate() 创建材质实例,并确保需要动态批处理的物体共享 同一个材质实例。public class DynamicBatchingExample : MonoBehaviour { public GameObject prefab; public int count = 100; public Material sharedMaterial; // 在 Inspector 中指定共享材质 void Start() { // 创建材质实例 Material materialInstance = new Material(sharedMaterial); for (int i = 0; i < count; i++) { GameObject go = Instantiate(prefab, transform); // 设置共享材质实例 go.GetComponent<Renderer>().material = materialInstance; go.transform.position = Random.insideUnitSphere * 10f; } } }
动态批处理示例场景:
创建一个场景,包含多个相同的 Sphere 物体 (顶点数量小于 300),并将它们赋予 同一个材质实例。运行游戏,在 Stats 窗口或 Profiler 中观察 Batches 数量,你会发现 Batches 数量有所降低 (但可能不如 Static Batching 明显)。
4.4.3.3 GPU Instancing (GPU 实例化)
原理:
GPU Instancing 是一种利用 GPU 的硬件特性,高效渲染 大量相同网格 的技术。它只需要一次 Draw Call,就可以渲染成千上万个相同的物体。
适用场景:
场景中存在大量 相同网格 的物体,例如草地、树叶、粒子、人群等。
物体可以拥有不同的 位置、旋转、缩放、颜色 等属性。
优点:
极大地减少 Draw Call 数量: 可以将成千上万个物体的 Draw Call 降低到一个。
GPU 渲染效率极高: 利用 GPU 硬件加速,渲染效率远高于传统的 Batching 技术。
支持动态属性: 可以为每个实例设置不同的属性,实现丰富的视觉效果。
缺点:
限制条件:
相同网格: GPU Instancing 只能渲染相同的网格。
Shader 支持: 需要使用支持 GPU Instancing 的 Shader (通常是 Surface Shader 或 Shader Graph 中的 Master Node 开启 Instancing)。
Shader 编写略有复杂: 需要在 Shader 中使用 unity_ObjectToWorld 矩阵和 UNITY_MATRIX_V 矩阵进行实例变换,并使用 UNITY_INSTANCING_BUFFER_START 和 UNITY_INSTANCING_BUFFER_END 宏定义实例属性。
代码实践 (GPU Instancing):
创建支持 GPU Instancing 的 Shader: 可以使用 Surface Shader 或 Shader Graph,并在 Shader 中开启 Instancing。
Surface Shader 示例:
Shader "Custom/InstancedShader" { Properties { _Color ("Color", Color) = (1,1,1,1) } SubShader { Tags { "RenderType"="Opaque" } LOD 100 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert instancing #pragma target 4.6 struct Input { float3 worldPos; float3 worldNormal; UNITY_INSTANCING_DATA }; UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color) UNITY_INSTANCING_BUFFER_END(Props) void vert (inout appdata_full v) { UNITY_SETUP_INSTANCE_ID(v); } void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = UNITY_ACCESS_INSTANCED_PROP(Props, _Color); o.Albedo = c.rgb; } ENDCG } FallBack "Diffuse" }
Shader Graph 示例: 在 Master Node 的 Graph Inspector 中,勾选 "Instancing" 复选框。
使用 Graphics.DrawMeshInstanced 或 Graphics.DrawMeshInstancedIndirect API 进行渲染: 在代码中使用 Graphics.DrawMeshInstanced 或 Graphics.DrawMeshInstancedIndirect API 替代 Graphics.DrawMesh 或 GameObject.CreatePrimitive 等方法进行渲染。
using UnityEngine; public class GPUInstancingExample : MonoBehaviour { public Mesh mesh; public Material material; public int count = 1000; void Update() { Matrix4x4[] matrices = new Matrix4x4[count]; Color[] colors = new Color[count]; for (int i = 0; i < count; i++) { matrices[i] = Matrix4x4.TRS(Random.insideUnitSphere * 20f, Quaternion.identity, Vector3.one); colors[i] = Color.Lerp(Color.red, Color.blue, (float)i / count); } // 创建 MaterialPropertyBlock 用于传递 per-instance 属性 MaterialPropertyBlock mpb = new MaterialPropertyBlock(); mpb.SetColorArray("_Color", colors); // 使用 Graphics.DrawMeshInstanced 进行 GPU Instancing 渲染 Graphics.DrawMeshInstanced(mesh, 0, material, matrices, count, mpb); } }
GPU Instancing 工作流程图 (Mermaid):
4.4.3.4 SRP Batcher (可编程渲染管线批处理)
SRP Batcher 是 Unity 2019.3 引入的,专门为 Scriptable Render Pipeline (SRP) 设计的高效批处理技术。它能够显著减少 CPU 开销,尤其是在使用 SRP 的项目中,性能提升非常明显。
SRP Batcher 的原理比较复杂,简单来说,它通过 减少渲染状态切换 和 优化 Shader 属性传递 的方式,实现了更高效的批处理。它能够自动处理材质、Shader Variants、Global Keywords 等,无需手动进行额外的设置。
使用 SRP Batcher 的前提:
使用 Scriptable Render Pipeline (例如 Universal Render Pipeline 或 High Definition Render Pipeline)。
使用兼容 SRP Batcher 的 Shader (通常是 SRP 默认的 Shader 或基于 Shader Graph 创建的 Shader)。
SRP Batcher 的优势:
更高效的批处理: 比 Static Batching 和 Dynamic Batching 更高效,CPU 开销更低。
自动处理状态切换: 自动处理材质、Shader Variants、Global Keywords 等,减少状态切换开销。
易于使用: 无需手动设置,SRP Batcher 会自动生效。
内容详解:
Batching 技术是减少 Draw Call,提升渲染性能的关键。选择合适的 Batching 技术取决于场景的需求和物体的特性。
Static Batching: 适用于静态场景,性能提升明显,但内存占用较高,不适用于动态物体。
Dynamic Batching: 适用于小型动态物体,性能提升有限,限制条件较多。
GPU Instancing: 适用于大量相同网格的物体,性能提升巨大,但需要编写支持 Instancing 的 Shader。
SRP Batcher: 适用于使用 SRP 的项目,性能提升非常明显,易于使用,是 SRP 项目的首选批处理技术。
4.4.4 性能优化总结与最佳实践
优先使用 Profiler 进行性能分析: 不要盲目优化,先使用 Profiler 找到真正的性能瓶颈。
减少 Draw Call 数量: Draw Call 是渲染性能的主要瓶颈之一,尽可能减少 Draw Call 数量。
合理使用 Batching 技术: 根据场景和物体的特性选择合适的 Batching 技术,例如 Static Batching, Dynamic Batching, GPU Instancing, SRP Batcher。
优化 Shader: 简化 Shader 逻辑,减少 Shader 指令数量,避免使用过于复杂的 Shader。
优化材质: 尽可能复用材质,减少材质实例数量,避免频繁的状态切换。
控制多边形数量: 合理控制模型的多边形数量,避免过度细分的模型。
使用 LOD (Level of Detail) 技术: 根据物体距离摄像机的远近,切换不同精度的模型。
优化阴影: 合理设置阴影参数,例如阴影分辨率、阴影距离等,避免不必要的阴影渲染开销。
优化透明物体: 透明物体的渲染开销较高,尽量减少透明物体的数量,或者使用半透明 Shader 替代完全透明 Shader。
使用 Occlusion Culling (遮挡剔除): 剔除被遮挡的物体,减少不必要的渲染开销。
使用 Lightmapping (光照贴图): 将静态光照烘焙到光照贴图中,减少实时光照的计算量。
优化脚本代码: 避免在 Update 函数中进行耗时操作,使用对象池、协程、Job System 等技术优化脚本性能。
定期进行性能测试和优化: 在开发过程中,定期进行性能测试,及时发现并解决性能问题。
结论:
性能优化是一个持续不断的过程,需要我们深入理解 Unity 的渲染原理和性能优化技术,并结合 Profiler 工具进行分析和实践。通过本章节的学习,相信你已经掌握了 Profiler 的使用方法,理解了 Draw Call 的概念和性能影响,并掌握了多种 Batching 技术。在未来的游戏开发中,灵活运用这些技术,你将能够打造出更加流畅、高效的 Unity3D 游戏。