【Unity Optimize】Unity中的优化工具和优化方法介绍

1 Unity项目优化的必要性

未经优化的Unity项目常会导致如下问题:

  • 包体大
  • 启动慢
  • 性能低
  • 耗电快
  • ……

因此,对项目优化是必要的且必须的,一些Unity项目的优化方案如下:

  • 合理利用资源:使用合适的纹理压缩、正交摄像机、减少多余的粒子特效等。
  • 优化场景:使用LOD技术、遮挡剔除、动态合并场景等方法优化场景,减少不必要的渲染、碰撞检测。
  • 使用对象池管理对象:避免创建和销毁游戏对象的过程中频繁地分配和释放内存。
  • 代码优化:减少内存分配、使用单例模式、避免使用GC Alloc等。
  • 用Asset Bundle:把资产打包成小块便于动态加载,减少初始下载时间和存储空间占用。
  • 移除未使用的资源:移除未使用的音频、贴图和代码等未使用的资源。
  • 资产压缩:压缩纹理和音频资源等。

本文将从Unity自带的优化工具入手,介绍如何对Unity项目进行优化,如Profiler窗口、Stats窗口、Frame Debugger窗口、Occlusion Culling(遮挡剔除)以及其它的一些优化方法。

2 Unity自带的优化工具

2.1 Profiler窗口

Profiler 窗口是一个非常强大的性能诊断工具,它提供了项目运行时的详细信息,包括FPS(每秒帧数)、 CPU利用率、内存占用等,以便分析性能瓶颈、进一步识别和优化性能问题,提高游戏的帧率和整体性能。

在Unity的菜单栏Window > Analysis > Profiler或者使用快捷键Ctrl+7打开Profiler窗口:


打开之后,在 Editor 窗口中运行项目,以便 Profiler 可以开始从项目中收集性能数据:
在这里插入图片描述
在Profiler窗口的左侧,有一系列的分析器,如 CPU Usage(CPU 的使用)、GPU Usage(图形处理器的使用)、Rendering(渲染)、Memory(内存的使用)、Audio(音频)、Video(视频)、Physics(物理)、Network(网络)、UI(用户界面)、Realtime GI(实时全局光照)、Virtual Texturing(虚拟纹理)、File(文件)和Asset(资源加载)。
在这里插入图片描述
Profiler窗口的下半部分显示当前选择的Profiler的详细信息。

如选中Rendering,显示的则是场景渲染相关的详细信息:

在这里插入图片描述
一般来说,可以根据项目目前遇到的瓶颈而选择合适的Profiler进行分析。如果资源的内存占用过大,则可以使用Memory Profiler得知哪些资产耗费了最多的资源;如果项目运行缓慢,则可以使用CPU Usage Profiler开始分析。

在Profiler窗口的顶部,有一些控件,允许我们启动和停止分析、启用和禁用分析功能以及浏览Profiler收集的数据。

在这里插入图片描述

在这里插入图片描述

Play Mode:对运行中的项目进行性能分析。

Edit Mode:对 Unity 编辑器进行性能分析。

<Enter IP>:在其他电脑或设备上对项目进行远程性能分析。

在这里插入图片描述:Record,记录Profiler以便进一步的调试和分析。

在这里插入图片描述:Deep Profiler,如果启用,Profiler将会分析所有的脚本。这样我们能够准确地知道在脚本中花费的时间。但是需注意,开启Deep Profiler会占用大量的内存,因此,项目的运行速度会较之前更慢。

:Clear,清除所有Profiler收集的数据。

在这里插入图片描述:Clear on Play,每次运行项目时清除之前Profiler收集的数据。

在这里插入图片描述:Load,从文件中加载二进制的分析信息,单击 Shift 将把信息追加到Profiler中。

在这里插入图片描述:Save将当前分析信息保存到二进制文件。

在这里插入图片描述:Arrows,在Profiler内捕获的数据中,按帧向前或向后逐步移动,可对每帧进行具体的定位和分析。

在这里插入图片描述:Current,返回当前帧。

这些控件可以让我们在项目运行的过程中收集产生的数据,在收集到所需数据后停止收集。然后我们可以使用Arrows控件逐步浏览收集的数据,直到找到可能存在性能问题的帧。

Profile Analyzer

2.2 Stats窗口

Stats 窗口能实时显示游戏帧率、渲染性能和资源使用等统计数据,这对于优化性能非常有用。

在Unity的Game视图的右上角可以找到Stats,点击以打开Stats窗口:
在这里插入图片描述
统计信息分为两类: Audio和Graphics。

Audio 部分包含关于场景当前帧的四个重要信息:

在这里插入图片描述

  • Level:当前声音分贝的等级。如果已静音,那么Level旁会显示(MUTED)。
    在这里插入图片描述 在这里插入图片描述

  • DSP load:DSP(Digital Signal Processing)负载表示同时处理的音频流的百分比。

    • 它用于衡量由音频处理器执行的数字信号处理所占用的CPU资源。当DSP负载升高时,表明音频处理器正在使用更多的CPU资源,这可能会影响游戏的运行速度和性能。
  • Clipping:音频失真的百分比。

    • Clipping是音频削波的缩写。在音频播放过程中,如果音量信号超过所设置的最大输出值,那么超过的部分就会被“削掉”,这通常会导致音质降低。在Unity中,Clipping统计了所有的削波操作,并显示为削波比率。 当削波发生时,它会降低音频的质量。
  • Stream load:读取和加载场景或GameObject所需的时间,通常应该保持较低的值。

    • 在Unity中,Stream Load表示读取流式资源的时间。当Stream Load增加时,这可能表明游戏读取和加载资源的速度较慢,可能会导致帧率降低。可以通过优化I/O和文件的加载等方法,以减少 Stream Load时间。

Graphics 部分包含如下的信息:

在这里插入图片描述

  • FPS:FPS(Frames Per Second)每秒帧数,即处理和渲染一帧所需的时间,以毫秒(ms)为单位。
    • 游戏性能通常被认为是流畅度,如果FPS值低于 30 ,可能会使游戏看起来不连贯或卡顿。
  • CPU:CPU 性能表示使用CPU的总时间(毫秒)以及总使用时间的百分比。
    • 可以通过此数据来识别CPU资源的利用率,并根据需要进行调整。
  • Batches:Batches数量表示一帧中绘制的批次数。Unity尝试将多个对象的渲染合并到一块内存进行批处理,以减少由于CPU 和 GPU切换而导致的额外开销。
    • 用于绘制的批次数越多,CPU 和 GPU 就越需要切换到渲染状态,这影响着游戏的性能。可以减少由于绘制数量增加而导致性能瓶颈的风险。
  • Tris:Tris(三角面)数量表示在整个游戏场景中渲染的三角面数量。
    • 渲染大量的三角面可能会导致性能问题,因为 GPU 需要处理更多的几何信息,也可能会导致阴影渲染和绘制开销问题。一般推荐三角面数不要超过50万(500k)。
  • Verts:Verts(顶点)数量表示在游戏场景中渲染的顶点数量。
    • 顶点数量也会影响性能,因为它涉及到渲染资源和GPU的存储和运算资源。可以使用此值来优化游戏场景的渲染和对象。
  • Screen:显示有多少像素需要在屏幕上进行渲染。
    • 屏幕像素越多,呈现其所需的 CPU 和 GPU 资源就越多。可以使用此数据以适应不同屏幕和分辨率,以保持高性能。
  • SetPass calls:在单帧中调用渲染通道的属性。
    • SetPass call应尽量减少,以保持游戏性能。
  • Shadow casters:产生阴影的对象数量。
    • 阴影渲染也同样影响游戏的性能,可以使用此数据来识别引起阴影问题的对象,并对其进行优化。
  • Visible skinned meshes:在单帧中处于可见状态的蒙皮网格数量。
    • 蒙皮网格在游戏性能中的占用率比较高,可以使用此数据来确定利用GPU硬件中蒙皮和动画功能的对象,以提高游戏性能。
  • Animations:在单帧中渲染动画的数量。

Stats 窗口提供了关于项目运行中实时反馈和统计的信息。我们可以通过它确定项目中可能导致性能问题的位置,并在 Profiler 窗口中进一步找到关键的问题所在。

2.3 Frame Debugger窗口

Frame Debugger(帧调试器)是一个用于分析和调试游戏画面渲染的工具。它能让我们深入了解游戏画面的渲染流程,并查看每个像素的颜色和深度信息,方便我们识别并找到性能瓶颈。在某些情况下,它还可以检测到一些常见的渲染错误,如材质设置错误、渲染状态错误等等。

在Unity的菜单栏Window > Analysis > Frame Debugger打开Frame Debugger窗口:

在这里插入图片描述

在弹出的窗口中点击左上角的Enable按钮以启用该功能:

在这里插入图片描述
单击窗口左侧的任意 DrawMesh 调用。当点击了一个 DrawMesh,它会用 DrawMesh 实际渲染的内容来更新游戏窗口:
在这里插入图片描述
Frame Debugger窗口被大致划分为左右两个部分,左侧是 DrawMesh 调用序列和后处理效果等其他事件,右侧提供了关于所选DrawMesh调用的进一步信息,例如几何细节和用于渲染的着色器等信息。
在这里插入图片描述

Frame Debugger窗口的工具栏:

在这里插入图片描述

  • Enable/Disable:开启/禁用Frame Debugger的开关。
  • Editor下拉菜单:可以通过 IP 预览从编辑器的Camera或远程编辑器中选择的 DrawMesh 调用。需注意,使用的设备必须支持多线程渲染(multi-threaded rendering),并且必须勾选 Development Build。另外,还需要勾选 Run in Background。
  • 滑动条:使用滑动条的方式切换 DrawMesh 调用层。
  • 左右箭头:使用左右箭头切换 DrawMesh 调用层。

另外,可以通过色彩通道和亮度进行渲染分离,再通过 Channels 和 Levels 功能进行更精细的调整。
在这里插入图片描述

  • Render Target:Frame Debugger 的渲染目标,允许我们改变渲染目标。当同时拥有多个渲染目标时,可以选择一个进行分析。
  • Channels:可以分别查看红色®、绿色(G)、蓝色(B)和透明度(A)通道,从而更好地了解游戏视图的颜色分布和通道权重,帮助解决渲染问题。另外,如果选择 Alpha 通道,可以查看对象的遮挡度(RT0)和光滑度(RT1)信息,这对于解决采用延迟渲染的游戏效果非常有用。
  • Levels:可以根据亮度值分离和聚焦屏幕区域。

注意,当渲染到 RenderTexture 时,Channels 和 Levels 功能才能使用。

当我们选择DrawMesh调用时,我们可以在右侧查看 ShaderProperties ,它可以显示正在使用的着色器(Shader)的状态和属性。

在这里插入图片描述

在Unity中,着色器可以分为多个阶段,例如顶点着色器(Vertex Shader)、片元着色器(Fragment Shader)等等。在渲染过程中,每个阶段的输入和输出都可能会产生变化,因此了解当前着色器的状态和属性非常重要。

通过ShaderProperties,我们可以查看目前使用的着色器、当前阶段、材质属性设置等信息。这样可以检查是否正确使用了着色器和材质属性,进而识别并解决一些渲染相关的问题。

最后,需要注意的是,由于Frame Debugger会降低游戏的性能,因此建议在开发时使用,而不是在发布版中使用。

3 其他优化方法

3.1 批处理(Batching)

批处理(Batching)是一种将类似处理任务分组处理的技术。

在Unity中,使用Sprite Atlas(图集)的Batching技术将每个合并的Sprite和UI元素打包成一个或多个贴图,这些贴图合并成一个大的整体,这有助于减少GPU的渲染数量,从而提高游戏性能。可以通过在Inspector中勾选"Batching"来启用批处理。

3.2 内存管理(Memory Handling)

在编写Unity脚本的过程中,内存管理是一个重要的问题。尤其是在开发手机游戏时,内存使用率对游戏性能有很大影响。

在Unity中,内存有两种管理方式:(Heap)和 (Stack)。

栈是一个临时存储和较小变量的内存区域,具有更快的访问速度,除了全局变量之外,最好只在需要的时候在使用变量的函数中声明变量。

int myEnergy;

一旦没再引用,栈中的内存就会自动释放。

而堆则用于较大或需要长时间存放的内存对象,但访问速度较慢。在使用关键字“new”创建对象时,内存从堆中分配。

List<string> myGuests = new List<string>();

堆中变量占用的内存需要是一个连续的块。如果声明的变量的连续内存不足,则释放不需要的堆内存,避免内存泄漏,影响游戏性能。如果可用内存仍然不足,则扩展堆。

不再需要用于存储变量数据的堆内存称为垃圾。释放这个不需要的内存称为垃圾回收(Garbage Collection)。垃圾回收通常根据需要进行,但也可以手动触发。垃圾回收的频率取决于多种因素,如需要释放的内存量等,但可能会在性能上造成一些影响。手动触发垃圾回收通常用于在性能影响较小的情况下释放内存。

3.3 对象池(Object Pooling)

对象池(Object Pooling)是一种优化垃圾回收的方法,用于在游戏中重复利用GameObject。

在游戏中,有些对象需要频繁地创建和删除,如子弹、火焰等特效,或是程序生成和可破坏的地形块。而在非游戏场景中,如动态填充和刷新的用户界面元素也可以采用对象池的方式。这样会导致内存分配和垃圾回收的频繁操作,从而导致游戏性能下降。

使用对象池可以在游戏初始化时预先创建游戏对象,然后在需要时重复利用这些对象,而不是每次都重新创建。这样可以减少内存分配和垃圾回收,从而提高游戏性能。

具体的使用方法可参考博客:【Unity Optimize】使用对象池(Object Pooling)优化项目

3.4 资源优化(Asset optimization)

资源优化是指减少Unity在加载、访问和卸载资源时需要执行的操作,从而使项目运行更加平稳和响应迅速。
Unity 要加载、访问和卸载资源所需的时间越少,项目就运行得越顺畅。

有以下的这些技巧对Unity中的资源进行优化:

  • 将所有 2D (Sprites 和 UI) 图形元素应打包到 Sprite Atlases 中。

    • Sprite Atlas 是一个大型纹理,包含多个 Sprites,图形处理单元 (GPU) 批量处理它们,而不是单独处理每个 Sprite。
    • 对于图形量很大的游戏,Sprite Atlases 可以按角色、世界或目的分开。
    • Sprite Atlas的具体使用方法可参考博客:【Unity Optimize】使用图集(Sprite Atlas)优化项目
  • 所有未打包在 Sprite Atlas 中的纹理应具有 2 的幂次尺寸,以利用 GPU 上的内存处理优化。它们不一定需要是正方形,但如果它们是正方形且为 2 的幂次进一步有助于优化。

  • 较长的音频(如背景音乐)应该将其导入设置中的 Load Type 设置为 Streaming,而不是 Decompress on Load 或 Compressed in Memory。这样做会造成可能稍有延迟的启动,但好处是只需要一小部分 RAM 来流式传输,而不是在内存中加载和解压缩整个歌曲。

如果遵循这些技巧,可以更好地处理内存,减少 Unity 对资源的负载,优化项目性能,进而提供更好的用户体验。

3.5 缓存(Caching)

缓存(Caching)是指将数据暂时存储起来,以便能够更快地重复使用。缓存所有需要经常访问的东西可提高游戏性能、加速读取速度并减少资源占用的内存,因为从硬盘加载大量资源是一个耗时的过程。通

例如,在Update循环中,不要使用GetComponent,而是声明该组件类型的数据成员,而在Start或Awake中调用GetComponent一次将其分配给数据成员。在Update中,只需引用数据成员即可。

3.6 Update, LateUpdate, and FixedUpdate的使用

Update、LateUpdate和FixedUpdate是Unity中重要的循环事件。

Update事件会在每一帧中触发,可用于控制游戏对象的移动、旋转和动画等。

LateUpdate事件是带有“Late”前缀的另一个事件,它在每一帧之后触发。它可以用来进行相机操作或其他需要在玩家输入操作之后进行的操作。

FixedUpdate事件调用频率与帧速率无关,它被用于物理引擎的模拟,在处理物理时使用。

不建议将所有 MonoBehavior 的逻辑全都放在 Update() 循环中,原因如下:

  • 可读性。将逻辑分解成清晰命名的函数并根据需要调用这些函数是一个好的做法。这种抽象让我们更容易跟踪逻辑的流程,根据需要重新排列步骤,并在代码中查找和调试问题区域。另一个好处是,可以模拟游戏逻辑流程并进行实际实现的调试。
  • 一致性。Update() 的循环次数取决于帧率,不稳定性会对物理模拟产生影响,而物理模拟最好应该在 FixedUpdate() 中实现。FixedUpdate() 以固定间隔运行,效果更佳。
  • 如果代码依赖于其他 MonoBehaviors 的 Update() 循环执行的操作,则最好将其放在 LateUpdate() 中。LateUpdate() 在所有 MonoBehaviors 的 Update() 函数运行后执行。其中一种用途是修正跟随玩家的摄像头的运动,以确保其保持在游戏世界的边界内。
  • Update() 循环每帧都要完整运行,因此会导致性能问题。不需要每帧运行的代码应该被封装在一个条件块中。例如,如果玩家受到伤害后被击退,我们不需要检查控制器输入,因为他们在这一帧内无法自行移动。

3.7 协程(Coroutines)

在Unity中,有时候某些代码片段可能需要在单个帧(frame)中运行完成并立即返回其结果,这些代码通常在 Update、FixedUpdate 或 LateUpdate 的回调函数中运行。然而,有些任务可能需要比一个帧长的时间来完成,或者只有在特定条件下才会执行。为了解决这些问题,Unity 提供了协程(Coroutine)机制,允许我们在适当的时候挂起并恢复代码执行。

协程(Coroutine)是使用 IEnumerator 返回类型的函数,并通过调用 yield return 语句进行挂起以在稍后恢复执行。通过将代码放置在协程中,使Unity的主循环更新其他游戏对象的帧的同时调用协程的函数,从而对执行时间没有限制。我们可以使用协程来执行一些长时间的任务,例如动画、复杂的算法、迭代生成程序(如地形或随机地图)或创建无限奔跑游戏的屏幕外平台。

另外,当需要定期执行一个任务时,例如在固定间隔时间内执行某个函数,Unity 提供了一种更简单的方式来实现这个目标:InvokeRepeating函数。这个函数仅需要两个参数:要调用的函数和调用之间的时间间隔。这个函数通常与 Start 函数一起使用,以避免每一帧都需要重新调用该函数,从而使游戏效率更高。它特别适用于周期性任务(例如波浪控制器在固定时间间隔内检查是否所有敌人已死亡的任务),或用于更新游戏中的UI元素(如屏幕计分板或生命计数器)。

4 总结

当我们开发一个项目时,不要一开始就想去优化所有的东西,这会干扰我们建立一个具备所需功能且可测试的项目;不必要的优化会浪费时间,甚至会导致一些错误。相反,应该在开发的过程中从一开始就将优化技巧融入到工作流程中。当完成一个可运行的项目时,应该使用Profiler窗口监控性能 并在目标设备上进行测试,只在需要的时候才进行优化。

换句话说,优化应该是一个持续的过程,应该在确保项目功能正常的基础上进行,而不是一次性完成。我们应该时刻监控项目的性能,并结合目标设备的特性有选择地针对特定的瓶颈进行优化,而不是匆忙地对整个项目进行不必要的优化。这样才能在保持项目可维护性和稳定性的同时,最大化地提升项目性能。

猜你喜欢

转载自blog.csdn.net/qq_41084756/article/details/130934118
今日推荐