4.4 性能优化 (Performance Optimization)


文档摘要

4.4 性能优化 (Performance Optimization) (Profiler, Draw Call, Batching) 4.4 性能优化 (Performance Optimization) 在Unity3D游戏开发中,性能优化是至关重要的环节。一个流畅、高效的游戏体验是吸引玩家并留住玩家的关键。性能不佳的游戏,即使拥有精美的画面和有趣的内容,也可能因为卡顿、掉帧等问题而让玩家感到沮丧。因此,作为开发者,我们需要深入理解性能优化的原理和方法,确保我们的游戏能够在各种设备上稳定运行。 4.4.1 Profiler:性能分析的利器 什么是 Profiler? Profiler 是 Unity 内置的强大的性能分析工具。

4.4 性能优化 (Performance Optimization) (Profiler, Draw Call, Batching)

4.4 性能优化 (Performance Optimization)

在Unity3D游戏开发中,性能优化是至关重要的环节。一个流畅、高效的游戏体验是吸引玩家并留住玩家的关键。性能不佳的游戏,即使拥有精美的画面和有趣的内容,也可能因为卡顿、掉帧等问题而让玩家感到沮丧。因此,作为开发者,我们需要深入理解性能优化的原理和方法,确保我们的游戏能够在各种设备上稳定运行。

4.4.1 Profiler:性能分析的利器

什么是 Profiler?

Profiler 是 Unity 内置的强大的性能分析工具。它可以实时监控并记录游戏运行时的各种性能数据,包括 CPU 使用率、GPU 使用率、内存占用、渲染数据、音频数据等等。通过 Profiler,我们可以清晰地了解游戏性能瓶颈所在,从而有针对性地进行优化。

Profiler 的重要性

  • 精准定位性能瓶颈: Profiler 能够详细展示各个模块的性能消耗,例如脚本、渲染、物理、动画等,帮助我们快速找到性能瓶颈。

  • 量化优化效果: 在进行优化后,可以通过 Profiler 再次分析,量化优化效果,确保优化措施真正有效。

  • 持续监控性能: 在开发过程中,可以持续使用 Profiler 监控性能变化,及时发现并解决潜在的性能问题。

  • 跨平台性能分析: Profiler 支持在 Editor 编辑器和真机设备上进行性能分析,帮助我们了解游戏在不同平台上的表现。

如何使用 Profiler?

  1. 打开 Profiler 窗口: 在 Unity 编辑器菜单栏中,选择 Window -> Profiler,即可打开 Profiler 窗口。

  2. 连接 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)。

  3. Profiler 窗口界面:

    Profiler 窗口主要分为两个区域:

    • 模块选择区域 (Modules): 位于窗口左侧,可以选择要监控的性能模块,例如 CPU Usage, Rendering, Memory, Audio, Physics 等。

    • 图表显示区域 (Charts): 位于窗口右侧,以图表的形式实时显示各个模块的性能数据。

  4. 常用的 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) 等。

  5. 录制 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,能够让我们告别盲目猜测,基于数据进行优化,从而更高效地提升游戏性能。

4.4.2 Draw Call:渲染性能的瓶颈

什么是 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 的有效手段。

4.4.3 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 对静态批处理的合并数量有限制,如果场景中静态物体数量过多,可能无法全部合并。

代码实践 (静态批处理):

  1. 标记物体为 Static: 在 Inspector 窗口中,选中要参与静态批处理的物体,勾选右上角的 "Static" 复选框。

  2. 确保物体使用相同的材质 (可选但强烈建议): 为了获得最佳的静态批处理效果,建议静态物体使用相同的材质。如果使用不同的材质,静态批处理仍然会生效,但可能会根据材质进行多次 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 较弱。

代码实践 (动态批处理):

  1. 确保物体使用相同的材质实例: 创建材质时,使用 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; } } }
  1. 确保物体满足 Dynamic Batching 的其他限制条件: 例如控制顶点数量在限制范围内,避免使用复杂的 Shader 等。

动态批处理示例场景:

创建一个场景,包含多个相同的 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_STARTUNITY_INSTANCING_BUFFER_END 宏定义实例属性。

代码实践 (GPU Instancing):

  1. 创建支持 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" 复选框。

  2. 使用 Graphics.DrawMeshInstancedGraphics.DrawMeshInstancedIndirect API 进行渲染: 在代码中使用 Graphics.DrawMeshInstancedGraphics.DrawMeshInstancedIndirect API 替代 Graphics.DrawMeshGameObject.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 游戏。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U