0


Unity性能优化与分析--CPU

1** 全局设置**

1.1 降低或禁用Accelerometer Frequency

Unity 每秒对移动端的加速度计进行几次池操作。如果在应用程序中不会使用,请将其禁用,或者降低其频率,从而提升性能。

如果在移动游戏中不会使用 Accelerometer Frequency,请务必将其禁用。

1.2 禁用不必要的Player或Quality****设置

Player 设置中,对不支持的平台禁用 Auto Graphics API,以便防止生成过多着色器变体。如果应用程序不支持,对较旧的 CPU 禁用 Target Architectures

Quality 设置中,禁用不需要的质量级别。

1.3 选择正确的帧率

移动端项目必须在帧率和电池续航时间以及热节流之间获得平衡。不需要将设备限值推向 60 fps,可以折衷以 30 fps 运行。Unity 默认移动端为 30 fps。

您也可以通过 Application.targetFrameRate 在运行时动态调整帧率。例如,您甚至可以将缓慢或相对静止的场景降至 30 fps 以下,而将玩游戏时的 fps 设置保留为较高值。

1.4 假设Vsync****已启用

移动平台不渲染半帧。即使在编辑器中禁用 Vsync (Project Settings > Quality), Vsync 在硬件级别也处于启用状态。如果 GPU 无法足够快地刷新,将保持当前帧,从而有效降低每秒帧数。

1.5 启用Prebake Collision Meshes

可以通过PlayerSetting选项中勾选Prebake Collision Meshes选项来在构建应用时预先Bake出碰撞网格。

这项设置可以减少加载/初始化的时间, 虽然会增加一些构建时间和包体积。

启用 Prebake Collision Meshes。

1.6 设置Vertex Compression

Vertex Compression就是顶点压缩机制, 可以根据需要选择压缩的数据

优点:他可以节省空间,运行效率更高

缺点:加载时间变长

2. 渲染模块

围绕渲染模块相关优化更全面的内容可以参考《Unity性能优化系列—渲染模块》。

2.1 多线程渲染

原理介绍:

一般情况下,在单线程渲染的流程中,在游戏每一帧运行过程中,主线程(CPU1)先执行Update,在这里做大量的逻辑更新,例如游戏AI、碰撞检测和动画更新等;然后执行Render,在这里做渲染相关的指令调用。在渲染时,主线程需要调用图形API更新渲染状态,例如设置Shader、纹理、矩阵和Alpha融合等,然后再执行DrawCall,所有的这些图形API调用都是与驱动层交互的,而驱动层维护着所有的渲染状态,这些API的调用有可能会触发驱动层的渲染状态地改变,从而发生卡顿。由于驱动层的状态对于上层调用是透明的,因此卡顿是否会发生以及卡顿发生的时间长短对于API的调用者(CPU1)来说都是未知的。而此时其它CPU有可能处于空闲等待的状态,从而造成浪费。因此可以将渲染部分抽离出来,放到其它的CPU中,形成单独的渲染线程,与逻辑线程同时进行,以减少主线程卡顿。

其大致的实现流程是,在主线程中调用的图形API被封装成命令,提交到渲染队列,这样就可以节省在主线程中调用图形API的开销,从而提高帧率;渲染线程从渲染队列获取渲染指令并执行调用图形API与驱动层交互,这部分交互耗时从主线程转到渲染线程。

注意事项:

  • 一般建议保持开启Project Settings的Multithreaded Rendering选项(多线程渲染)。
  • 开启多线程渲染时,CPU等待GPU完成工作的耗时会被统计到Gfx.WaitForPresent函数中,而关闭多线程渲染时这一部分耗时则被主要统计到Graphics.PresentAndSync中。
  • 在项目开发和测试阶段可以考虑暂时性地关闭多线程渲染并打包测试,从而更直观地反映出渲染模块存在的性能瓶颈。
  • 对于正常开启了多线程渲染的项目,Gfx.WaitForPresent的耗时走向也有相当的参考意义。测试中局部的GPU压力越大,CPU等待GPU完成工作的时间也就越长,Gfx.WaitForPresent的耗时也就越高。所以,当Gfx.WaitForPresent存在数十甚至上百毫秒地持续耗时时,说明对应场景的GPU压力较大。
  • GPU压力过大也会使得渲染模块CPU端的主函数耗时(Camera.Render和RenderPipelineManager.DoRenderLoop_Internal)整体相应上升

2.2 同屏渲染面片数

影响渲染效率的两个最基本的参数无疑就是Triangle和DrawCall。

通常情况下,Triangle面片数和GPU渲染耗时是成正比的,尤其需要关注。

同屏Triangle面片数, 低端机型建议在25万面以内, 高端机型建议在60万面以内

我们需要更关注渲染面数而不是模型面数

可以使用Frame Debugger对重点帧的渲染物体进行排查。

常见的优化方案是:

  • 在制作上严格控制网格资源的面片数,尤其是一些角色和地形的模型,应严格警惕数万面及以上的网格;
  • 通过LOD工具减少场景中的面片数——比如在低端机上使用低模、减少场景中相对不重要的小物件的展示——进而降低渲染的开销。

严格警惕使用(渲染面数翻倍):

  • Shader多个渲染Pass
  • 多个摄像机同时渲染
  • SSAO, Reflection等后处理效果

2.3 Batch(DrawCall)

在Unity中,我们需要区分DrawCall和Batch。在一个Batch中会存在有多个DrawCall,出现这种情况时我们往往更关心Batch的数量,因为它才是把渲染数据提交给GPU的单位,也是我们需要优化和控制数量的真正对象。

推荐课程和文章:

《Unity移动游戏项目优化案例分析(上)》

Unity合批(Batching)的限制与失败原因汇总2.0

降低Batch的方式:

2.3.1 动态合批

对于小网格,Unity 在 CPU 上分组和转换顶点,然后一次性绘制它们。 条件:

  • 少于 900 个顶点属性和不超过 300 个顶点
  • 除了渲染阴影对象外,相同材质,不同材质实例也不能合并
  • 具有光照贴图的游戏对象如果有附加渲染器参数时,如果需要动态合批这些对象,他们必须指向相同的光照贴图位置。
  • 有多Shader Pass的游戏对象无法做动态合批
  • 延迟渲染下不支持动态合批

优点:

缺点:

  • 受多个光照影响的游戏对象,满足动态合批条件合批后,只会受一个光源的影响
  • CPU开销会增大, 因为每一帧都要去查找要批处理的小网格。
2.3.2 静态合批

Unity 可以减少所有共享相同材质的网格的绘制调用。 条件:

  • 不同Mesh,只要使用相同的材质球即可。

优点:

  • 节省顶点信息地绑定;节省几何信息地传递;相邻材质相同时, ,节省材质地传递。

缺点:

  • 若合并的Mesh中存在重复资源,则会使得合并后内存占用变大。
  • 64000个顶点限制
  • 影响Culling剔除
  • 运行时合并,则生成Combine Mesh的过程会造成CPU短时间峰值
2.3.3 SRP Batcher

*在Universal Render Pipeline Asset***中启用 **SRP Batcher。这样可以大幅提高 CPU 渲染速度。

条件:

  • 不同Mesh,只要使用相同的Shader且变体一样即可。
  • 图形API版本要求
  • 必须是SRP可编程渲染管线
  • 粒子对象不能合批
  • 使用MaterialPropertyBlocks的游戏对象不能合批
  • Shader必须是compatible的

优点:

  • 节省Uniform Buffer的写入操作;按Shader分Batch,预先生成Uniform Buffer,Batch内部无CPU Write。

缺点:

  • Constant Buffer(CBuffer)的显存固定开销;不支持MaterialPropertyBlock。
2.3.4 GPU Instancing

适用于渲染同种大量怪物的需求,合批的同时能够降低动画模块的耗时。 条件:

  • 相同的Mesh,且使用相同的材质球。
  • 图形API版本要求
  • 与SRPBatcher不兼容
  • 不同绘制API的参数与绘制个数不同

缺点:

  • 渲染顶点数较少的网格时,效率可能会较差。(测试使用)
  • 可能存在负优化,反而使DrawCall上升;Instancing有时候被打乱,可以自己分组用API渲染。

2.4 Shader.CreateGPUProgram

它是Shader第一次渲染时产生的耗时,其耗时与渲染Shader的复杂程度相关。经常造成渲染模块中的大多数函数峰值。

可以使用ShaderVariantCollection收集要用到的Shader变体并进行预加载,从而避免触发此API的调用

开发者可以使用Profiler,选中触发调用Shader.CreateGPUProgram的帧来查看具体是哪些Shader触发了该API。

推荐课程:

《一种Shader变体收集和打包编译优化的思路》

2.5 Culling

绝大多数情况下,Culling本身耗时并不显眼,它的意义在于反映一些与渲染相关的问题。

(1)相机数量多 当渲染模块主函数的堆栈中Culling耗时的占比比较高(一般项目中在10%-20%左右)。

(2)场景中小物件多 Culling耗时与场景中的GameObject小物件数量的相关性比较大。这种情况建议研发团队优化场景制作方式 ,关注场景中是否存在过多小物件,导致Culling耗时增高。

可以考虑采用以下方法:

  • 动态加载, 分块显示
  • Culling Group
  • Culling Distance

(3)遮挡剔除 Occlusion Culling 如果项目使用了多线程渲染且开启了Occlusion Culling,通常会导致子线程的压力过大而使整体Culling过高。

由于Occlusion Culling需要根据场景中的物体计算遮挡关系,因此开启Occlusion Culling虽然降低了渲染消耗,其本身的性能开销却也是值得注意的,并不一定适用于所有场景。

建议开发者选择性地关闭一部分Occlusion Culling去测试一下渲染数据的整体消耗进行对比,再决定是否需要开启这个功能。

(4)包围盒更新 FinalizeUpdateRendererBoundingVolumes Culling的堆栈中有时出现的FinalizeUpdateRendererBoundingVolumes为包围盒更新耗时。一般常见于:

  • Skinned Mesh更新
  • 粒子系统的包围盒更新。

(5)PostProcessingLayer.OnPreCull/WaterReflection.OnWillRenderObject PostProcessLayer.OnPreCull这一方法和项目中使用的PostProcessing Stack相关。

减少耗时方法:

PostProcessManager.cs中添加静态变量GlobalNeedUpdateSettings,在切场景的时候通过设置PostProcessManager.GlobalNeedUpdateSettings为true来UpdateSettings。这样就可以避免每帧都做UpdateSettings操作,从而减少一部分耗时。

WaterReflection.OnWillRenderObject则是项目中使用到的水面反射效果的相关耗时,若该项耗时较高,可以关注一下实现方式上是否有可优化的空间,比如去除一些不必要的粒子、小物件等的反射渲染。

3. UI模块

课程学习:

《Unity性能优化 — UI模块》

3.1 UGUI EventSystem.Update()

3.1.1 点击耗时高, 使用Profiler深度分析

通常是因为调用了其它较为耗时的函数引起。因此需要通过以下方法来对所触发的逻辑进行进一步地检测,从而排查出具体是哪一个子函数或者代码段造成的高耗时。

  • 开启Profiler深度分析
  • 添加Profiler.BeginSample/EndSample打点
  • GOT Online服务+UWA API打点
3.1.2 轮询耗时高, 禁用Raycast Target
  • 必要的需要交互UI组件才开启“Raycast Target”
  • 开启“Raycast Targets”的UI组件越少,层级越浅,性能越好
  • 对于复杂的控件,尽量在根节点开启“Raycast Target”
  • 对于嵌套的Canvas,OverrideSorting属性会打断射线,可以降低层级遍历的成本

尽可能禁用 Raycast Target。

3.2 UGUI Canvas.SendWillRenderCanvases()

Canvas.SendWillRenderCanvases函数的耗时代表的是UI元素自身变化带来的更新耗时

造成该耗时的原因:

  • 替换图片
  • 文本或颜色发生变化

以下情况不会造成该耗时:

  • UI元素发生位移、旋转或者缩放

优化方案:

(1)降低频繁更新的UI元素的频率 例如小地图的怪物标记、角色或者怪物的血条等,可以控制逻辑在变动超过某个阈值时才更新UI的显示.

例如技能CD效果,伤害飘字等, 可以控制隔帧更新。

(2)尽量让复杂的UI不要发生变动

Text文本控件的Rich Text、Outline, Shadow效果(造成顶点数量翻倍)

Image文本控件的Image Type为Tiled 这些UI元素因为顶点数量非常多,一旦更新便会有较高的耗时。

如飘动的伤害数字,可以考虑将其做成固定的美术字,这样顶点数量就不会翻N倍。

(3)关注Font.CacheFontForText()

该函数往往会造成一些耗时峰值。该API主要是生成动态字体Font Texture的开销,在运行时突发高耗时,很有可能是一次性写入很多新的字符,导致Font Texture纹理扩容。

可以从减少字体种类、减少字体字号、提前显示常用字以扩充动态字体FontTexture等方式去优化这一项的耗时。

3.3 UGUI Canvas.BuildBatch()

Canvas.BuildBatch(网格重建)为UI元素合并的Mesh需要改变时所产生的调用。

Canvas.BuildBatch在主线程发起UI网格合并,而合并过程是在子线程中处理的,当子线程压力过大,或者合并的UI网格过于复杂的时候,会在主线程产生等待,等待的耗时会被统计到EmitWorldScreenspaceCameraGeometry中。这两个函数产生高耗时,说明发生重建的Canvas非常复杂

造成该耗时的原因:

  • 替换图片
  • 文本或颜色发生变化
  • UI元素发生位移、旋转或者缩放

处理方法:

  • 将Canvas适当拆分,让需要进行更新操作的元素和其它元素不在一个Canvas下, 这样静态的Canvas由于缓存不会发生网格更新,从而降低网格更新的复杂度,减少网格重建的耗时。
  • 如果只需要关闭Canvas的可见性,请禁用 Canvas 组件而不是游戏对象。这样就不必重新构建网格和顶点。
  • 可以拆分使用多个同级或嵌套的Canvas来减少Canvas的BuildBatch复杂度

3.4 UGUI CanvasRenderer.SyncTransform()

有些项目的部分帧中CanvasRenderer.SyncTransform调用频繁。如下图,CanvasRenderer.SyncTransform调用次数多达1017次。当Canvas.SyncTransform触发次数非常频繁时,会导致它的父节点UGUI.Rendering.UpdateBathes产生非常高的耗时。

原因:

在Unity 2018版本及以后的版本中,Canvas下某个UI元素调用SetActive(false改成true)会导致该Canvas下的其它UI元素触发SyncTransform,从而导致UI更新的整体开销上升,在Unity 2017的版本中只会导致该UI元素本身触发SyncTransform。

解决方法:

  • 不要使用材质的透明度控制显隐,因为那样UI网格依然在绘制
  • 尽量使用SetScale(0或者1)来代替SetActive(false或者true)。

3.5 UI合批

在不减少UI元素的前提下,控制DrawCall的问题,其实也就是如何使得UI元素尽量合批的问题。

一般而言,战斗场景中的UI DrawCall控制到40-50左右为最佳。

在UGUI的制作过程中,常常会发生无法合批的现象, 建议关注以下几点:

(1)同一Canvas下的UI元素才能合批。不同Canvas即使Order in Layer相同也不合批,所以UI的合理规划和制作非常重要;

(2)尽量整合并制作图集,从而使得不同UI元素的材质图集一致。图集中的按钮、图标等需要使用图片的比较小的UI元素,完全可以整合并制作图集。当它们密集地同时出现时,就有效降低了DrawCall;

(3)在同一Canvas下、且材质和图集一致的前提下,避免UI元素重叠。简单概括就是,应使得符合合批条件的UI元素的“层级深度”相同;

(4)将相关UI的Pos Z尽量统一设置为0,Z值不为0的UI元素只能与Hierarchy中相邻元素尝试合批,所以容易打断合批。

(5)对于Alpha为0的Image,需要勾选其CanvasRender组件上的Cull Transparent Mesh选项,否则依然会产生DrawCall且容易打断合批。

(6)将所有可能打断合批的层移到最下边的图层

推荐课程:

《详解UGUI DrawCall计算和Rebuild操作优化》

**3.6 限制GraphicRaycaster **

输入事件(如屏上触摸或单击)需要 GraphicRaycaster 组件。它只是循环处理屏幕上的每个输入点,检查它是否在 UI 的 RectTransform 之内。

从层级视图的顶层画布中移除默认的 GraphicRaycaster。只向需要交互的各元素(按钮、滚动矩形等)添加 GraphicRaycaster

禁用在默认情况下处于活动状态的 Ignore Reversed Graphics。

3.7 酌情选择使用布局组

布局组的更新很低效,应少量使用。如果内容是动态的,应完全避免不用,而是使用锚点进行比例布局。或者,创建自定义代码,在Layout Group 组件设置 UI 之后,将该组件禁用。

如果动态元素确实需要使用布局组(水平、垂直、网格),应避免嵌套它们,从而改善性能。

布局组会降低性能,尤其是在嵌套时。

3.8 RenderMode

  • 尽量Overlay模式,减少Camera调用的开销机。
  • 如果非要用World Space模式, 请务必填充 Event Camera, 并修改LayerMask, 只渲染UI层, 将 Render Camera 字段留空会使 Unity 填充 Camera.main,这会导致不必要的开销。

3.9 UI控件优化注意事项

  • 使用全屏UI 时,则禁用摄像机对 3D 场景的渲染。并且禁用隐藏在顶层画布之后的所有背景画布元素。
  • 可以考虑在全屏 UI 过程中降低 Application.targetFrameRate。
  • 如果是较大的背景图的UI元素建议也要使用Sprite的九宫格拉伸处理,充分减小UI Sprite大小,提高UI Atlas图集利用率
  • 在使用非全屏但模态对话框时,建议使用OnDemandRendering接口,对渲染进行降频。
  • 优化裁剪UI Shader,根据实际使用需求移除多余特性关键字。

3.10 滚动视图Scroll View优化

  • 使用RectMask2d组件裁剪
  • 使用基于位置的对象池作为实例化缓存(UI无限列表)

4. 物理模块

围绕物理模块相关优化更全面的内容可以参考《Unity性能优化 — 物理模块》。

留意 Profiler 的 Physics 模块是否有性能问题。

4.1 Auto Simulation

在Unity 2017.4版本之后,物理模拟的设置选项Auto Simulation被开放并且默认开启,即项目过程中总是默认进行着物理模拟。但在一些情况下,这部分的耗时是浪费的。

判断Auto Simulation耗时是否被浪费的一个标准就是Contacts(碰撞对)数量,碰撞对的数量越多,则物理系统的CPU耗时越大。但在很多项目中,我们都检测到在整个游戏过程中Contacts数量始终为0。

处理方法:

可以关闭Auto Simulation来进行测试。如果关闭Auto Simulation并不会对游戏逻辑产生任何影响,则说明可以节省这方面的耗时。

4.2 Auto Sync Transform

在Transform组件发生改变时,强制进行物理系统同步。相当于在修改Transform后,立即执行一次对物理对象的模拟更新,这样会增加物理运算覆盖,一般不开启。不开启不代表不更新,一般等到FixedUpdate的过程再更新

如果项目需要使用射线检测,那么在关闭Auto Simulation后需要开启Auto Sync Transforms,来保证射线检测可以正常作用。

4.3 Reuse Collision Callbacks

尽量开启,在物理引擎对所有碰撞进行回调时,会重用之前的Collision碰撞结果的实例,而不会为每个碰撞回调重新创建碰撞结果的实例,由于大多数情况碰撞结果实例只是数值上的变化,重用已经碰撞好的碰撞结果实例,可以降低托管堆上的GC开销

4.4 简化Layer Collision Matrix

尽可能简化 Layer Collision Matrix。

4.5 FixedUpdate

Unity物理模拟过程的主要耗时函数是在FixedUpdate中的,也就是说,当每帧该函数调用次数越高、物理更新次数也就越频繁,每帧的耗时也就相应地高。

这里我们需要先知道物理系统本身的特性,即当游戏上一帧卡顿时,Unity会在当前帧非常靠前的阶段连续调用N次FixedUpdate.PhysicsFixedUpdate.

我们一般不建议在FixedUpdate中写过多游戏逻辑。

修改固定时间间隔

参数介绍:

Fixed Timestep: 最小更新间隔

Maximum Allowed Timestep: 最大允许时间, 它决定了单帧FixedUpdate最大调用次数,该值越小,调用次数越少。默认为17次,最好控制到5次以下

4.6 Contacts 碰撞对

就像上面提到的,如果我们确实用到物理模拟,则一般碰撞对的数量越多,物理系统的CPU耗时也就越大。所以,严格控制碰撞对数量对于降低物理模块耗时非常重要。

首先,很多项目中可能存在一些不必要的Rigidbody组件,在开发者不知情的地方造成了不必要的碰撞,从而产生了耗时浪费;另外,可以检查修改Project Settings的Physics设置中的Layer Collision Matrix,取消不必要的层之间的碰撞检测,将Contacts数量尽可能降低。

4.7 简化碰撞体

  • 尽量少使用MeshCollider,可以用简单Collider代替,即使用多个简单Collider组合代替也要比复杂的MeshCollider来的高效
  • MeshCollider是基于三角形面的碰撞
  • MeshCollider生成的碰撞体网格占用内存也较高
  • MeshCollider即使要用也要尽量保障其是静态物体

4.8 使用物理方法移动刚体

使用类方法(如 MovePositionAddForce)来移动 Rigidbody 对象。直接转换其 Transform 组件可能导致重新计算物理世界,在复杂场景中,这样需要较大开销。

FixedUpdate 中而不是 Update 中移动物理体。

**4.9 Kinematic **

  • Kinematic刚体消耗比动态刚体更少的性能.
  • Kinematic对象仅拥有一部分物理效果, 不受物理引擎中力的影响,但可以对其他RigidBody施加物理影响。

4.9 通过 Physics Debugger 实现可视化

使用 Physics Debug 窗口 (Window > Analysis > Physics Debugger) 可帮助故障检查有问题的碰撞体或者出现差异的情况。下面是一个颜色编码的指示器,指示哪些游戏对象可以相互碰撞。

Physics Debugger 可帮助您可视化物理对象能够相互交互的方式。

有关更多信息,请参阅 Unity 文档中的物理调试可视化。

5. 动画模块

参考课程:

《Unity性能优化 — 动画模块》

《Unity移动游戏中动画系统的性能优化》

5.1 Animator和Animation性能对比

  • 对于骨骼动画且曲线较多的动画,使用Animator的性能是要比Animation要好的,因为Animator是支持多线程计算的,而且Animator可以通过开启Optimized GameObjects进行优化.
  • 对于比较简单的类似于移动旋转这样的动画,使用Animation控制则比Animator要性能高效一些。

5.2 BakeMesh

对于一两千面这样面数较少且动画时长较短的对象,如MOBA、SLG中的小兵等,可考虑用SkinnedMeshRenderer.BakeMesh的方案,用内存换CPU耗时。其原理是将一个蒙皮动画的某个时间点上的动作,Bake成一个不带蒙皮的Mesh,从而可以通过自定义的采样间隔,将一段动画转成一组Mesh序列帧。而后在播放动画时只需选择最近的采样点(即一个Mesh)进行赋值即可,从而省去了骨骼更新与蒙皮计算的时间(几乎没有动画,只是赋值的动作)。整个操作比较适合于面片数小的人物,因为此举省去了蒙皮计算。其作用在于:用内存换取计算时间,在场景中大量出现同一个带动画的模型时,效果会非常明显。该方法的缺点是内存的占用极大地受到模型顶点数、动画总时长及采样间隔的限制。因此,该方法只适用于顶点数较少,且动画总时长较短的模型。同时,Bake的时间较长,需要在加载场景时完成。

5.3 Active Animator数量

Active状态的Animator个数会极大地影响动画模块的耗时,而且是一个可量化的重要标准,控制其数量到一个相对合理的值是我们优化动画模块的重要手段。需要开发者结合画面排查对应的数量是否合理。

5.3.1 Culling Mode 动画剔除

控制Active Animator的一个方法是针对每个动画组件调整合理的Animator.CullingMode选项设置。

参数介绍与选择:

**AlwaysAnimate: **默认选项, 当前物体不管是不是在视域体内,或者在视域体被LOD Culling掉了,Animator的所有东西都仍然更新;其中,UI动画一定要选AlwaysAnimate,不然会出现异常表现。

**CullUpdateTransforms: **当物体不在视域体内,或者被LOD Culling掉后,逻辑继续更新,就表示状态机是更新的,动画资源中连线的条件等等也都是会更新和判断的;但是Retarget、IK和从C++回传Transform这些显示层的更新就不做了。所以,在不影响表现的前提下把部分动画组件尝试设置成CullUpdateTransforms可以节省物体不可见时动画模块的显示层耗时。

可以考虑同时禁用SkinMesh Renderer的Update When Offscreen属性, 让角色在视域体之外时蒙皮不更新

**CullComplete: **完全不更新,适用于场景中相对不重要的动画效果,在低端机上需要保留显示但可以考虑让其静止的物体,分级地选用该设置。

5.3.2 UI动画, 尽量用DOTween而不是Animator

建议使用DOTween制作简单的UI动画,如改变颜色、缩放、移动等效果,性能比Animator要好得多。

5.4 Animator.ApplyBuiltinRootMotion()

在Animators.Update的堆栈中,有时会看到Animator.ApplyBuiltinRootMotion占比过高,这一项通常和项目中开启了Apply Root Motion的模型动画相关。

如果其动画不需要产生位移,则不必开启Apply Root Motion选项。

5.5 Animator.Initialize

Animator.Initialize API会在含有Animator组件的GameObject被Active和Instantiate时触发

不建议过于频繁地对含有Animator的GameObject进行Deactive/Active GameObject操作。

对于频繁实例化的角色,可尝试通过缓冲池的方式进行处理,在需要隐藏角色时,不直接Deactive角色的GameObject,而是Disable Animator组件,并把GameObject移到屏幕外;

在需要隐藏UI动画时,不直接Deactive UI对象,而是将其SetScale(0)并且移出屏幕的方式,也不会触发Animator.Initialize。

5.6 Meshskinning.Update和Animators.WriteJob

网格资源对于动画模块耗时的影响是十分显著的。

Meshskinning.Update耗时较高时。主要因素为蒙皮网格的骨骼数和面片数偏高

可以针对网格资源进行减面和LOD分级。

很多项目中角色的骨骼节点的Transform一直都是在场景中存在的,这样在Native层计算完它们的Transform后,会回传给C#层,从而产生一定的耗时。如果场景中角色数量较多,骨骼节点的回传会产生一定的开销,体现在动画模块的主函数之一PreLateUpdate.DirectorUpdateAnimationEnd的Animators.WriteJob子函数上。

可以考虑勾选FBX资源中Rig页签下的Optimize Game Objects设置项,将骨骼节点“隐藏”,从而减少这部分的耗时。

5.7 GPU Skinning/Compute Skinning

Unity引擎原生的GPU Skinning设置项(新版Unity中为Compute Skinning),理论上会在一定程度上改变网格和动画的更新方法以优化对骨骼动画的处理,但从针对移动平台的多项测试结果来看,无论是在iOS还是安卓平台上,多个Unity版本提供的GPU Skinning对性能的提升效果都不明显,甚至存在负优化的现象。在Unity的迭代中已对其逐步优化,将相关操作放到渲染线程中进行,但其实用性还需要进一步考察。

对于大量同种怪物的需求,可以考虑使用自己实现的《GPU Skinning 加速骨骼动画》,和UWA开源库中的GPU Instancing来进行渲染,这样既可以降低Animator.Update耗时,又能达到合批的效果。

5.8 使用通用还是人形骨架

默认情况下,Unity 通过通用骨架导入动画模型,但在动画化角色时,开发人员常常切换为人形骨架。

人形骨架每一帧(即使未使用)都计算反向动力学和动画重定向,占用的 CPU 时间比等效的通用骨架多 30-50%。如果不需要这些特定人形骨架功能,请使用通用骨架。

5.9 其他注意事项

  • 不要使用字符串来查询Animator
  • 使用曲线标记来处理动画事件
  • 使用Target Marching函数来协助处理动画

6. 粒子系统

围绕粒子系统相关优化更全面的内容可以参考《粒子系统优化——如何优化你的技能特效》。

6.1 Playing粒子系统数量

UWA统计了粒子系统数量和Playing状态的粒子系统数量。前者是指内存中所有的ParticleSystem的总数量,包含正在播放的和处于缓存池中的;后者指的是正在播放的ParticleSystem组件的数量,这个包含了屏幕内和屏幕外的,我们建议在一帧中出现的数量峰值不超过50(1GB机型)。

针对这两个数值,我们一方面关注粒子系统数量峰值是否偏高,可选中某一峰值帧查看到底是哪些粒子系统缓存着、是否都合理、是否有过度缓存的现象;另一方面关注Playing数量峰值是否偏高,可选中某一峰值帧查看到底是哪些粒子系统在播放、是否都合理、是否能做些制作上的优化(具体见下文GPU部分中的讨论)。

6.2 Prewarm

ParticleSystem.Prewarm的耗时有时也需要关注。当有粒子系统开启了Prewarm选项,其在场景中实例化或者由Deactive转为Active时,会立即执行一次完整的模拟。

但Prewarm的操作通常都有一定的耗时,经测试,大量开启Prewarm的粒子系统同时SetActive时会造成耗时峰值。建议在不必要的情况下,将其关闭。


7. 加载模块

围绕加载模块相关优化更全面的内容可以参考《Unity性能优化系列—加载与资源管理》。

7.1 Shader加载

(1)Shader.Parse Shader.Parse是指Shader加载进行解析的操作

频繁触发的原因:

  • 打包AssetBundle的时候,Shader被打进了多个不同的AssetBundle中而没有进行依赖打包,这样当这些AssetBundle中的资源进行加载的时候,会被动加载这些Shader,就进行了多次“重复的”Shader.Parse,所以同一种Shader就在内存中有多份了。
  • 同一个Shader多次地加载卸载,没有缓存住。假设AssetBundle进行了主动打包,生成了公共的AssetBundle,这样在内存中只有这一份Shader,但是因为这个Shader加载完后(也就是Shader.Parse)没有进行缓存,用完马上被卸载了。下次再用到这个Shader的时候,内存里没有这个Shader了,那就必须再重新加载进来,这样同样的一个Shader加载解析了多次,就造成了多次的Shader.Parse。

解决方法:

  • 把这些会冗余的Shader依赖打包进一个公共的AssetBundle包
  • 统一在游戏开始时加载并缓存。
  • 对于Unity内置的Shader,只要是变体数量不多的,可以放进Project Settings中的Always Included中去,从而避免这一类Shader的冗余和重复解析。

(2)Shader.CreateGPUProgram 该API也会在加载模块主函数甚至UI模块、逻辑代码的堆栈中出现。相关的讨论上文已经涉及,优化方法相同,不再赘述。

7.2 Resources.UnloadUnusedAssets

该API会在场景切换时被Unity自动调用,一般单次调用耗时较高,通常情况下不建议手动调用。

但在部分不进行场景切换或用Additive加载场景的项目中,不会调用该API,从而使得项目整体资源数量和内存有上升趋势。对于这种情况则可以考虑每5-10min手动调用一次。

Resources.UnloadUnusedAssets的底层运作机理是,对于每个资源,遍历所有Hierarchy Tree中的GameObject结点,以及堆内存中的对象,检测该资源是否被某个GameObject或对象(组件)所使用,如果全部都没有使用,则引擎才会认定其为Unused资源,进而进行卸载操作。简单来讲,Resources.UnloadUnusedAssets的单次耗时大致随着((GameObject数量+Mono对象数量)*Asset数量)的乘积变大而变大。

因此,该过程极为耗时,并且场景中GameObject/Asset数量越高,堆内存中的对象数越高,其开销也就越大。对此,我们的建议如下:

(1)Resources.UnloadAsset/AssetBundle.Unload(True) 研发团队可尝试在游戏运行时,通过Resources.UnloadAsset/AssetBundle.Unload(True)来去除已经确定不再使用的某一资源,这两个API的效率很高,同时也可以降低Resources.UnloadUnusedAssets统一处理时的压力,进而减少切换场景时该API的耗时;

(2)严格控制场景中材质资源和粒子系统的使用数量。 专门提到这两种资源,因为在大多数项目中,虽然它们的内存占用一般不是大头,但往往资源数量远高于其他类型的资源,很容易达到数千的数量级,从而对单次Resources.UnloadUnusedAssets耗时有较大贡献。

(3)降低驻留的堆内存。 堆内存中的对象数量同样会显著影响Resources.UnloadUnusedAssets的耗时,这在上文也已经讨论过。

7.3 加载AssetBundle

使用AssetBundle加载资源是目前移动端项目中比较普遍的做法。

而其中,应尽量用LZ4压缩格式打包AssetBundle,并用LoadFromFile的方式加载。经测试,这种组合下即便是较大的AssetBundle包(包含10张1024*1024的纹理),其加载耗时也仅零点几毫秒。而使用其他加载方式,如LoadFromMemory,加载耗时则上升到了数十毫秒;而使用WebRequest加载则会造成AssetBundle包的驻留内存显著上升。

这是因为,LoadFromFile是一种高效的API,用于从本地存储(如硬盘或SD卡)加载未压缩或LZ4压缩格式的AssetBundle。

在桌面独立平台、控制台和移动平台上,API将只加载AssetBundle的头部,并将剩余的数据留在磁盘上。AssetBundle的Objects会按需加载,比如:加载方法(例如:AssetBundle.Load)被调用或其InstanceID被间接引用的时候。在这种情况下,不会消耗过多的内存。

但在Editor环境下,API还是会把整个AssetBundle加载到内存中,就像读取磁盘上的字节和使用AssetBundle.LoadFromMemoryAsync一样。如果在Editor中对项目进行了分析,此API可能会导致在AssetBundle加载期间出现内存尖峰。但这不应影响设备上的性能,在做优化之前,这些尖峰应该在设备上重新再测试一遍。

要注意,这个API只针对未压缩或LZ4压缩格式,因为如果使用LZMA压缩,它是针对整个生成后的数据包进行压缩的,所以在未解压之前是无法拿到AssetBundle的头信息的。

由于LoadFromMemory的加载效率相较其他的接口而言,耗时明显增大,因此我们不建议大规模使用,而且堆内存会变大。如果确实有对AssetBundle文件加密的需求,可以考虑仅对重要的配置文件、代码等进行加密,对纹理、网格等资源文件则无需进行加密。因为目前市面上已经存在一些工具可以从更底层的方式来获取和导出渲染相关的资源,如纹理、网格等,因此,对于这部分的资源加密并不是十分的必要性。

在UWA GOT Online Resource模式下的资源管理页面中可以排查加载耗时较高的AssetBundle,从而排查和优化加载方式、压缩格式、包体过大等问题,或者对反复加载的AssetBundle考虑予以缓存。

7.4 加载资源

有关加载资源所造成的耗时,若加载策略比较合理,则一般发生在游戏一开始和场景切换时,往往不会造成严重的性能瓶颈。但不排除一些情况需要予以关注,那么可以把资源加载耗时的排序作为依据进行排查。

对于单次加载耗时过高的资源,比如达到数百毫秒甚至几秒时,就应考察这类资源是否过于复杂,从制作上考虑予以精简。

对于反复频繁加载且耗时不低的资源,则应该在第一次加载后予以缓存,避免重复加载造成的开销。

值得一提的是,在Unity的异步加载中有时会出现每帧进行加载所能占用的最高耗时被限制,但主线程中却在空转的现象。尤其是在切场景的时候集中进行异步加载,有时会耗费几十甚至数十秒的时间,但其中大部分时间是被空转浪费的。这是因为控制异步加载每帧最高耗时的API Application.backgroundLoadingPriority默认值为BelowNormal,每帧最多只加载4ms。此时一般建议把该值调为High,即最多50ms每帧。

在UWA GOT Online Resource模式下的资源管理页面中可以排查加载耗时较高的资源,从而排查和优化加载方式、资源过于复杂等问题,或者对反复加载的资源考虑予以缓存。

7.5 实例化和销毁

实例化同样主要存在单个资源实例化耗时过高或某个资源反复频繁实例化的现象。根据耗时多少排列后,针对疑似有问题的资源,前者考虑简化,或者可以考虑分帧操作,比如对于一个较为复杂的UI Prefab,可以考虑改为先实例化显眼的、重要的界面和按钮,而翻页后的内容、装饰图标等再进行实例化;后者则建立缓存池,使用显隐操作来代替频繁的实例化。

在UWA GOT Online Resource模式下的资源管理页面中可以排查实例化耗时较高的资源,从而排查和优化资源过于复杂的问题,或者对反复实例化的资源考虑予以缓存。

7.6 激活和隐藏

激活和隐藏的耗时本身不高,但如果单帧的操作次数过多就需要予以关注。可能出于游戏逻辑中的一些判断和条件不够合理,很多项目中往往会出现某一种资源的显隐操作次数过多,且其中SetActive(True)远比SetActive(False)次数多得多、或者反之的现象,亦即存在大量不必要的SetActive调用。由于SetActive API会产生C#和Native的跨层调用,所以一旦数量一多,其耗时仍然是很可观的。针对这种情况,除了应该检查逻辑上是否可以优化外,还可以考虑在逻辑中建立状态缓存,在调用该API之前先判断资源当前的激活状态。相当于使用逻辑的开销代替该API的开销,相对耗时更低一些。

在UWA GOT Online Resource模式下的资源管理页面中可以排查激活隐藏操作较频繁的资源,从而排查和优化相关逻辑和调用。

** 参考文献:**

Unity移动端游戏性能优化简谱_UWA学堂

《Unity性能优化》系列课程 | Unity 中文课堂

移动游戏优化指南 | Unity 中文课堂


本文转载自: https://blog.csdn.net/qq_41281201/article/details/136371702
版权归原作者 五杀时间到了 所有, 如有侵权,请联系我们删除。

“Unity性能优化与分析--CPU”的评论:

还没有评论