共计 18863 个字符,预计需要花费 48 分钟才能阅读完成。
《Unity 挪动端游戏性能优化简谱》从 Unity 挪动端游戏优化的一些根底探讨登程,例举和剖析了近几年基于 Unity 开发的挪动端游戏我的项目中最为常见的局部性能问题,并展现了如何应用 UWA 的性能检测工具确定和解决这些问题。内容包含了性能优化的根本逻辑、UWA 性能检测工具和常见性能问题,心愿能提供给 Unity 开发者更多高效的研发办法和实战经验。
明天向大家介绍文章第三局部:以引擎模块为划分的 CPU 耗时调优,共 9 大节,蕴含了渲染模块、UI 模块、物理模块、动画模块、粒子系统、加载模块、逻辑代码、Lua 等多个模块等常见的游戏 CPU 耗时调优解说。
(全文长约 14115 字,预计浏览工夫约 30 分钟)
文章第一局部《Unity 挪动端游戏性能优化简谱之 前言》、第二局部《Unity 挪动端游戏性能优化简谱之 常见游戏内存管制》可戳此回顾,残缺内容可返回 UWA 学堂查看。
1. 总览
1.1 模块划分
UWA 将 CPU 中工作内容明确、耗时占比个别较高的函数整顿划分为:渲染、UI、物理、动画、粒子、加载、逻辑等模块。但这并不意味着模块之间的工作相互独立毫无关联。举例而言,渲染模块的性能压力势必受到简单的 UI 和粒子影响,而加载模块的很多操作实际上都是在逻辑中调用并实现的。
划分模块有利于咱们确认问题、找到重点。与此同时,也要建设起模块之间的关联,有助于更高效地解决问题。
1.2 耗时瓶颈
当一个我的项目因为 CPU 端性能瓶颈而产生帧率偏低、卡顿显著的景象时,如何提炼出哪个模块的哪个问题是造成性能瓶颈的次要问题就成了要害。只管咱们曾经对引擎中次要模块做了整顿,各个模块间会呈现的问题还是会千奇百怪不可一以概之,而且它们对 CPU 性能压力的奉献也不尽相同。那么咱们就须要对什么样的耗时能够认为是潜在的性能瓶颈有精确的认知。
在挪动端我的项目中,咱们 CPU 端性能优化的指标是可能在中低端机型上大部分工夫跑满 30 帧的晦涩游戏过程。为了达成这一指标,简略做一下除法就失去咱们的 CPU 耗时均值应管制在 33ms 以下。当然,这并不意味着 CPU 均值曾经在 33ms 以下的我的项目就曾经把 CPU 耗时管制的很好了。游戏运行过程中性能压力点是不同的,可能一系列 UI 界面中压力很小、但反过来游戏中最重要的战斗场景中帧率很低、又或者是存在大量几百毫秒甚至几秒的卡顿,而最终均匀下来依然低于 33ms。
为此,UWA 认为,在一次测试中,当 33ms 及以上耗时的帧数占总帧数的 10% 以下时,能够认为我的项目 CPU 性能整体管制在失常范畴内。而这个占比越高,阐明以后我的项目的 CPU 性能瓶颈越重大。
以上的探讨内容次要是围绕着咱们对 CPU 性能的宏观的优化指标,和内存一样,咱们仍要联合具体模块的具体数据来排查和解决我的项目中理论存在的问题。
2. 渲染模块
围绕渲染模块相干优化更全面的内容能够参考《Unity 性能优化系列—渲染模块》。
2.1 多线程渲染
个别状况下,在单线程渲染的流程中,在游戏每一帧运行过程中,主线程(CPU1)先执行 Update,在这里做大量的逻辑更新,例如游戏 AI、碰撞检测和动画更新等;而后执行 Render,在这里做渲染相干的指令调用。在渲染时,主线程须要调用图形 API 更新渲染状态,例如设置 Shader、纹理、矩阵和 Alpha 交融等,而后再执行 DrawCall,所有的这些图形 API 调用都是与驱动层交互的,而驱动层保护着所有的渲染状态,这些 API 的调用有可能会触发驱动层的渲染状态地扭转,从而产生卡顿。因为驱动层的状态对于下层调用是通明的,因而卡顿是否会产生以及卡顿产生的工夫长短对于 API 的调用者(CPU1)来说都是未知的。而此时其它 CPU 有可能处于闲暇期待的状态,从而造成节约。因而能够将渲染局部抽离进去,放到其它的 CPU 中,造成独自的渲染线程,与逻辑线程同时进行,以缩小主线程卡顿。
其大抵的实现流程是,在主线程中调用的图形 API 被封装成命令,提交到渲染队列,这样就能够节俭在主线程中调用图形 API 的开销,从而进步帧率;渲染线程从渲染队列获取渲染指令并执行调用图形 API 与驱动层交互,这部分交互耗时从主线程转到渲染线程。
而 Unity 在 Project Settings 中反对且默认开启了 Multithreaded Rendering,个别倡议放弃开启。在 UWA 的大量测试数据中,还是发现有局部我的项目敞开了多线程渲染。开启多线程渲染时,CPU 期待 GPU 实现工作的耗时会被统计到 Gfx.WaitForPresent 函数中,而敞开多线程渲染时这一部分耗时则被次要统计到 Graphics.PresentAndSync 中。所以,我的项目中是否统计到 Gfx.WaitForPresent 函数耗时是判断是否开启了多线程渲染的一个根据。特地地,在我的项目开发和测试阶段能够思考暂时性地敞开多线程渲染并打包测试,从而更直观地反映出渲染模块存在的性能瓶颈。
对于失常开启了多线程渲染的我的项目,Gfx.WaitForPresent 的耗时走向也有相当的参考意义。测试中部分的 GPU 压力越大,CPU 期待 GPU 实现工作的工夫也就越长,Gfx.WaitForPresent 的耗时也就越高。所以,当 Gfx.WaitForPresent 存在数十甚至上百毫秒地继续耗时时,阐明对应场景的 GPU 压力较大。
另外,依据 UWA 的大量我的项目和测试教训,GPU 压力过大也会使得渲染模块 CPU 端的主函数耗时(Camera.Render 和 RenderPipelineManager.DoRenderLoop_Internal)整体相应回升。咱们会在最初专门探讨 GPU 局部的优化。
2.2 同屏渲染面片数
影响渲染效率的两个最根本的参数无疑就是 Triangle 和 DrawCall。
通常状况下,Triangle 面片数和 GPU 渲染耗时是成正比的,而对于大部分我的项目来说,不通明 Triangle 数量又往往远比半透明 Triangle 要多,尤其须要关注。UWA 个别倡议在低端机型上将同屏渲染面片数管制在 25 万面以内,即使是高端机也不倡议超过 60 万面。当应用工具发现部分同屏渲染面片数过高后,能够联合 Frame Debugger 对重点帧的渲染物体进行排查。
常见的优化计划是,在制作上须要严格控制网格资源的面片数,尤其是一些角色和地形的模型,应严格警觉数万面及以上的网格;另外,一个很好的办法是一通过 LOD 工具缩小场景中的面片数——比方在低端机上应用低模、缩小场景中绝对不重要的小物件的展现——进而升高渲染的开销。
须要指出的是,UWA 工具所关注和统计的面片数量并不是以后帧场景模型的面片数,而是以后帧所渲染的面片数,其数值不仅与模型面片数无关,也和渲染次数相干,更加直观地反映出同屏渲染面片数造成的渲染压力。例如:场景中的网格模型面片数为 1 万,而其应用的 Shader 领有 2 个渲染 Pass,或者有 2 个相机对其同时渲染;又或者应用了 SSAO、Reflection 等后处理成果中的一个,那么此处所显示的 Triangle 数值将为 2 万。所以,在低端机上应严格警觉这些一下就会使同屏渲染面片数加倍的操作,即使对于高端机也应做好衡量,三思而后用。
2.3 Batch(DrawCall)
在 Unity 中,咱们须要辨别 DrawCall 和 Batch。在一个 Batch 中会存在有多个 DrawCall,呈现这种状况时咱们往往更关怀 Batch 的数量,因为它才是把渲染数据提交给 GPU 的单位,也是咱们须要优化和管制数量的真正对象。
升高 Batch 的形式通常有动静合批、动态合批、SRP Batcher 和 GPU Instancing 这四种,围绕 Batch 优化的探讨较为简单,再写一篇文章也不为过,所以本文不再开展来探讨,但在 UWA DAY 2020 中咱们具体探讨和分享了 DrawCall 与 Batch 的关系以及这 4 种 Batching 的应用详解,供大家参考:《Unity 移动游戏我的项目优化案例剖析(上)》。
上面简略总结动态合批、SRP Batcher 和 GPU Instancing 的合批条件和优缺点。
(1)动态合批
条件:不同 Mesh,只有应用雷同的材质球即可。
长处:节俭顶点信息地绑定;节俭几何信息地传递;相邻材质雷同时,,节俭材质地传递。
毛病:离线合并时,若合并的 Mesh 中存在反复资源,则容易使得合并后包体变大;运行时合并,则生成 Combine Mesh 的过程会造成 CPU 短时间峰值;同样的,若合并的 Mesh 中存在反复资源,则会使得合并后内存占用变大。
(2)SRP Batcher
条件:不同 Mesh,只有应用雷同的 Shader 且变体一样即可。
长处:节俭 Uniform Buffer 的写入操作;按 Shader 分 Batch,事后生成 Uniform Buffer,Batch 外部无 CPU Write。
毛病:Constant Buffer(CBuffer)的显存固定开销;不反对 MaterialPropertyBlock。
(3)GPU Instancing
条件:雷同的 Mesh,且应用雷同的材质球。
长处:实用于渲染同种大量怪物的需要,合批的同时可能升高动画模块的耗时。
毛病:可能存在负优化,反而使 DrawCall 回升;Instancing 有时候被打乱,能够本人分组用 API 渲染。
2.4 Shader.CreateGPUProgram
该 API 经常在渲染模块主函数的堆栈中呈现,并造成渲染模块中的大多数函数峰值。它是 Shader 第一次渲染时产生的耗时,其耗时与渲染 Shader 的复杂程度相干。当它在游戏过程中被调用并且造成较高的耗时峰值时应引起留神。
对此,咱们能够将 Shader 通过 ShaderVariantCollection 收集要用到的变体并进行 AssetBundle 打包。在将该 ShaderVariantCollection 资源加载进内存后,通过在游戏后期场景调用 ShaderVariantCollection.WarmUp 来触发 Shader.CreateGPUProgram,并将此 SVC 进行缓存,从而防止在游戏运行时触发此 API 的调用、防止部分的 CPU 高耗时。
然而即使是曾经做过以上操作的我的项目也常会检测到运行时偶然的该 API 耗时峰值,阐明存在一些“漏网之鱼”。开发者能够联合 Profiler 的 Timeline 模式,选中触发调用 Shader.CreateGPUProgram 的帧来查看具体是哪些 Shader 触发了该 API,能够参考《一种 Shader 变体收集和打包编译优化的思路》。
2.5 Culling
绝大多数状况下,Culling 自身耗时并不显眼,它的意义在于反映一些与渲染相干的问题。
(1)相机数量多
当渲染模块主函数的堆栈中 Culling 耗时的占比比拟高(个别我的项目中在 10%-20% 左右)。
(2)场景中小物件多
Culling 耗时与场景中的 GameObject 小物件数量的相关性比拟大。这种状况倡议研发团队优化场景制作形式,关注场景中是否存在过多小物件,导致 Culling 耗时增高。能够思考采纳动静加载、分块显示,或者 Culling Group、Culling Distance 等办法优化 Culling 的耗时。
(3)Occlusion Culling
如果我的项目应用了多线程渲染且开启了 Occlusion Culling,通常会导致子线程的压力过大而使整体 Culling 过高。
因为 Occlusion Culling 须要依据场景中的物体计算遮挡关系,因而开启 Occlusion Culling 尽管升高了渲染耗费,其自身的性能开销却也是值得注意的,并不一定实用于所有场景。这种状况倡议开发者选择性地敞开一部分 Occlusion Culling 去测试一下渲染数据的整体耗费进行比照,再决定是否须要开启这个性能。
(4)突围盒更新
Culling 的堆栈中有时呈现的 FinalizeUpdateRendererBoundingVolumes 为突围盒更新耗时。个别常见于 Skinned Mesh 和粒子系统的突围盒更新上。如果该 API 呈现很频繁,则要通过截图去排查此时是否有较大量的 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 框架有 UGUI、NGUI 以及应用越来越多的 FairyGUI。本文次要从应用最多的 UGUI 来进行阐明。围绕 UGUI 相干优化更全面的内容能够参考《Unity 性能优化 — UI 模块》。
3.1 UGUI EventSystem.Update
EventSystem.Update 函数为 UGUI 的事件零碎耗时,其耗时偏高时次要关注以下两个因素:
(1)触发调用耗时高
作为 UGUI 事件零碎的主函数,该函数次要是在触摸开释时触发,当自身有较高的 CPU 开销时,通常都是因为调用了其它较为耗时的函数引起。因而须要通过增加 Profiler.BeginSample/EndSample 打点或者 GOT Online 服务 +UWA API 打点来对所触发的逻辑进行进一步地检测,从而排查出具体是哪一个子函数或者代码段造成的高耗时。
(2)轮询耗时高
所有 UGUI 组件在创立时都默认开启了 Raycast Target 这一选项,实际上是为承受事件响应做好了筹备。事实上,大部分比方 Image、Text 类型的 UI 组件是不会参加事件响应的,但依然会在鼠标 / 手指划过或悬停时参加轮询,所以通过模仿射线检测判断 UI 组件是否被划过或悬停,造成不必要的耗时。尤其在我的项目中 UI 组件比拟多时,敞开不参加事件响应的组件的 Raycast Target 设置,能够无效升高 EventSystem.Update()耗时。
3.2 UGUI Canvas.SendWillRenderCanvases
Canvas.SendWillRenderCanvases 函数的耗时代表的是 UI 元素本身变动带来的更新耗时,这是须要和 Canvas.BuildBatch(见下文)的网格重建的耗时所辨别的。
继续的高耗时往往是因为 UI 元素过于简单且更新过于频繁造成。UI 元素的本身更新包含:替换图片、文本或色彩发生变化等等。UI 元素产生位移、旋转或者缩放并不会引起该函数有开销。该函数的耗时取决于 UI 元素产生更新的数量以及 UI 元素的复杂度,因而要优化此函数的开销通常能够从如下几点着手:
(1)升高频繁更新的 UI 元素的频率
比方小地图的怪物标记、角色或者怪物的血条等,能够管制逻辑在变动超过某个阈值时才更新 UI 的显示,再比方技能 CD 成果,挫伤飘字等管制隔帧更新。
(2)尽量让简单的 UI 不要产生变动
如某些字符串特地多且又应用了 Rich Text、Outline 或者 Shadow 成果的 Text,Image Type 为 Tiled 的 Image 等。这些 UI 元素因为顶点数量十分多,一旦更新便会有较高的耗时。如果某些成果须要应用 Outline 或者 Shadowmap,然而却又频繁的变动,如飘动的挫伤数字,能够思考将其做成固定的美术字,这样顶点数量就不会翻 N 倍。
(3)关注 Font.CacheFontForText
该函数往往会造成一些耗时峰值。该 API 次要是生成动静字体 Font Texture 的开销,在运行时突发高耗时,很有可能是一次性写入很多新的字符,导致 Font Texture 纹理扩容。能够从缩小字体品种、缩小字体字号、提前显示常用字以裁减动静字体 FontTexture 等形式去优化这一项的耗时。
3.3 UGUI Canvas.BuildBatch
Canvas.BuildBatch 为 UI 元素合并的 Mesh 须要扭转时所产生的调用。通常之前所提到的 Canvas.SendWillRenderCanvases()的调用都会引起 Canvas.BuildBatch 的调用。另外,Canvas 中的 UI 元素产生挪动也会引起 Canvas.BuildBatch 的调用。
Canvas.BuildBatch 是在主线程发动 UI 网格合并,具体的合并过程是在子线程中解决的,当子线程压力过大,或者合并的 UI 网格过于简单的时候,会在主线程产生期待,期待的耗时会被统计到 EmitWorldScreenspaceCameraGeometry 中。
这两个函数产生高耗时,阐明产生重建的 Canvas 非常复杂,此时须要将 Canvas 进行细分解决,通常是将动态的元素放在一个 Canvas 中,将产生更新的 UI 元素放入一个 Canvas 中,这样动态的 Canvas 因为缓存不会产生网格更新,从而升高网格更新的复杂度,缩小网格重建的耗时。
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 元素(如 Image、Text)特地多的 Canvas,须要留神是否存在一些 UI 元素在频繁地 SetActive,对于这种状况倡议应用 SetScale(0 或者 1)来代替 SetActive(false 或者 true)。或者,也能够将 Canvas 适当拆分,让须要进行 SetActive(true)操作的元素和其它元素不在一个 Canvas 下,就不会频繁调用 SyncTransform 了。
3.5 UGUI UI DrawCall
通常战斗场景中其它模块耗时压力大,此时 UI 模块更要认真管制性能开销。一般而言,战斗场景中的 UI DrawCall 管制到 40-50 左右为最佳。
在不缩小 UI 元素的前提下,管制 DrawCall 的问题,其实也就是如何使得 UI 元素尽量合批的问题。个别的合批要求材质雷同,而在 UI 中却经常会产生明明是应用同一材质、同一图集制作的 UI 元素却无奈合批的景象。这其实和 UGUI DrawCall 的计算原理无关。具体的原理介绍能够参考 UWA 学堂的这篇课程《详解 UGUI DrawCall 计算和 Rebuild 操作优化》。
在 UGUI 的制作过程中,倡议关注以下几点:
(1)同一 Canvas 下的 UI 元素能力合批。不同 Canvas 即便 Order in Layer 雷同也不合批,所以 UI 的正当布局和制作十分重要;
(2)尽量整合并制作图集,从而使得不同 UI 元素的材质图集统一。图集中的按钮、图标等须要应用图片的比拟小的 UI 元素,齐全能够整合并制作图集。当它们密集地同时呈现时,就无效升高了 DrawCall;
(3)在同一 Canvas 下、且材质和图集统一的前提下,防止层级交叉。简略概括就是,应使得合乎合批条件的 UI 元素的“层级深度”雷同;
(4)将相干 UI 的 Pos Z 尽量对立设置为 0,Z 值不为 0 的 UI 元素只能与 Hierarchy 中相邻元素尝试合批,所以容易打断合批。
(5)对于 Alpha 为 0 的 Image,须要勾选其 CanvasRender 组件上的 Cull Transparent Mesh 选项,否则仍然会产生 DrawCall 且容易打断合批。
4. 物理模块
围绕物理模块相干优化更全面的内容能够参考《Unity 性能优化 — 物理模块》。
4.1 Auto Simulation
在 Unity 2017.4 版本之后,物理模仿的设置选项 Auto Simulation 被凋谢并且默认开启,即我的项目过程中总是默认进行着物理模仿。但在一些状况下,这部分的耗时是节约的。
判断物理模仿耗时是否被节约的一个规范就是 Contacts 数量,即游戏运行时碰撞对数量。一般来说,碰撞对的数量越多,则物理零碎的 CPU 耗时越大。但在很多挪动端我的项目中,咱们都检测到在整个游戏过程中 Contacts 数量始终为 0。
在这种状况下,开发者能够敞开物理的主动模仿来进行测试。如果敞开 Auto Simulation 并不会对游戏逻辑产生任何影响,在游戏过程中仍然能够进行很好地对话、战斗等,则阐明能够节俭这方面的耗时。同时也须要阐明的是,如果我的项目须要应用射线检测,那么在敞开 Auto Simulation 后须要开启 Auto Sync Transforms,来保障射线检测能够失常作用。
4.2 物理更新次数
Unity 物理模仿过程的次要耗时函数是在 FixedUpdate 中的,也就是说,当每帧该函数调用次数越高、物理更新次数也就越频繁,每帧的耗时也就相应地高。
物理更新次数,或者说 FixedUpdate 的每帧调用次数,是和 Unity Project Settings 的 Time 设置中最小更新距离(Fixed Timestep)以及最大容许工夫(Maximum Allowed Timestep)相干的。这里咱们须要先晓得物理零碎自身的个性,即当游戏上一帧卡登时,Unity 会在以后帧十分靠前的阶段间断调用 N 次 FixedUpdate.PhysicsFixedUpdate,Maximum Allowed Timestep 的意义就在于限度物理更新的次数。它决定了单帧物理最大调用次数,该值越小,单帧物理最大调用次数越少。当初设置这两个值别离为 20ms 和 100ms,那么当某一帧耗时 30ms 时,物理更新只会执行 1 次;耗时 200ms 时也只会执行 5 次。
所以一个卓有成效的办法是调整这两个参数的设置,尤其是管制更新次数的下限(默认为 17 次,最好管制到 5 次以下),物理模块的耗时就不会过高;另一方面则是先优化其它模块的 CPU 耗时,当我的项目运行过程中耗时过高的帧很少,则 FixedUpdate 也不会总是达到每帧更新次数的下限。这对于其它 FixedUpdate 中的函数是同理的,也是基于这种起因,咱们个别不倡议在 FixedUpdate 中写过多游戏逻辑。
4.3 Contacts
就像下面提到的,如果咱们的确用到物理模仿,则个别碰撞对的数量越多,物理零碎的 CPU 耗时也就越大。所以,严格控制碰撞对数量对于升高物理模块耗时十分重要。
首先,很多我的项目中可能存在一些不必要的 Rigidbody 组件,在开发者不知情的中央造成了不必要的碰撞,从而产生了耗时节约;另外,能够查看批改 Project Settings 的 Physics 设置中的 Layer Collision Matrix,勾销不必要的层之间的碰撞检测,将 Contacts 数量尽可能升高。
5. 动画模块
围绕动画模块相干优化更全面的内容能够参考《Unity 性能优化 — 动画模块》。
5.1 Mecanim 动画零碎
Mechanic 动画零碎是 Unity 公司从 Unity 4.0 之后开始引入的新版动画零碎(应用 Animator 管制动画),相比于 Legacy 的 Animation 控制系统,在性能上,Mecanim 动画零碎次要有以下几点劣势:
(1)针对人形角色提供了一套非凡的工作流,包含 Avatar 的创立以及 Muscles 肌肉的调节;
(2)动画重定向(Retarting)的能力,能够十分不便地把一个动画从一个角色模型利用到其余角色模型上;
(3)提供了可视化的 Animator 编辑器,能够快捷预览和创立动画片段;
(4)更加不便地创立状态机以及状态之间 Transition 的转换;
(5)便于操作的混合树性能。
在性能上,对于骨骼动画且曲线较多的动画,应用 Animator 的性能是要比 Animation 要好的,因为 Animator 是反对多线程计算的,而且 Animator 能够通过开启 Optimized GameObjects 进行优化,具体细节能够参考 UWA 学堂的课程《Unity 移动游戏中动画零碎的性能优化》。相同,对于比较简单的相似于挪动旋转这样的动画,应用 Animation 管制则比 Animator 要高效一些。
5.2 BakeMesh
对于一两千面这样面数较少且动画时长较短的对象,如 MOBA、SLG 中的小兵等,可思考用 SkinnedMeshRenderer.BakeMesh 的计划,用内存换 CPU 耗时。其原理是将一个蒙皮动画的某个工夫点上的动作,Bake 成一个不带蒙皮的 Mesh,从而能够通过自定义的采样距离,将一段动画转成一组 Mesh 序列帧。而后在播放动画时只需抉择最近的采样点(即一个 Mesh)进行赋值即可,从而省去了骨骼更新与蒙皮计算的工夫(简直没有动画,只是赋值的动作)。整个操作比拟适宜于面片数小的人物,因为此举省去了蒙皮计算。其作用在于:用内存换取计算工夫,在场景中大量呈现同一个带动画的模型时,成果会非常明显。该办法的毛病是内存的占用极大地受到模型顶点数、动画总时长及采样距离的限度。因而,该办法只实用于顶点数较少,且动画总时长较短的模型。同时,Bake 的工夫较长,须要在加载场景时实现。
5.3 Active Animator 数量
Active 状态的 Animator 个数会极大地影响动画模块的耗时,而且是一个可量化的重要规范,管制其数量到一个绝对正当的值是咱们优化动画模块的重要伎俩。须要开发者联合画面排查对应的数量是否正当。
(1)Animator Culling Mode
管制 Active Animator 的一个办法是针对每个动画组件调整正当的 Animator.CullingMode 设置。该项设置一共有三个选项:AlwaysAnimate、CullUpdateTransforms 和 CullComplete。
默认的 AlwaysAnimate 使得以后物体不论是不是在视域体内,或者在视域体被 LOD Culling 掉了,Animator 的所有货色都依然更新;其中,UI 动画肯定要选 AlwaysAnimate,不然会出现异常体现。
而设置为 CullUpdateTransforms 时,当物体不在视域体内,或者被 LOD Culling 掉后,逻辑持续更新,就示意状态机是更新的,动画资源中连线的条件等等也都是会更新和判断的;然而 Retarget、IK 和从 C ++ 回传 Transform 这些显示层的更新就不做了。所以,在不影响体现的前提下把局部动画组件尝试设置成 CullUpdateTransforms 能够节俭物体不可见时动画模块的显示层耗时。
最初,CullComplete 就是齐全不更新了,实用于场景中绝对不重要的动画成果,在低端机上须要保留显示但能够思考让其静止的物体,分级地选用该设置。
(2)DOTween 插件
很多时候,UI 动画也会奉献大量的 Active Animator。针对一些简略的 UI 动画,如扭转色彩、缩放、挪动等成果,UWA 倡议改用 DOTween 制作。经测试,性能比原生的 UI 动画要好得多。
5.4 开启 Apply Root Motion 的 Animator 数量
在 Animators.Update 的堆栈中,有时会看到 Animator.ApplyBuiltinRootMotion 占比过高,这一项通常和我的项目中开启了 Apply Root Motion 的模型动画相干。如果其动画不须要产生位移,则不用开启此选项。
5.5 Animator.Initialize
Animator.Initialize API 会在含有 Animator 组件的 GameObject 被 Active 和 Instantiate 时触发,耗时较高。因而尤其是在战斗场景中不倡议过于频繁地对含有 Animator 的 GameObject 进行 Deactive/Active GameObject 操作。对于频繁实例化的角色和 UI,可尝试通过缓冲池的形式进行解决,在须要暗藏角色时,不间接 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 耗时,又能达到合批的成果。
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 加载进行解析的操作,如果此操作较为频繁,通常是因为 Shader 的反复加载导致的,这里的反复能够了解为 2 层意思。
第一层是因为 Shader 的冗余导致的,通常是因为打包 AssetBundle 的时候,Shader 被被动打进了多个不同的 AssetBundle 中而没有进行依赖打包,这样当这些 AssetBundle 中的资源进行加载的时候,会被动加载这些 Shader,就进行了屡次“反复的”Shader.Parse,所以同一种 Shader 就在内存中有多份了,这就是冗余了。
要去除这种冗余的办法也很简略,就是把这些会冗余的 Shader 依赖打包进一个公共的 AssetBundle 包。这样就会被动打包了,而不是被动进入某些应用了这个 Shader 的包体中。如果对这个 Shader 进行了被动打包,那么其它应用了这个 Shader 的 AssetBundle 中就只会对这个 Shader 打进去的公共 AssetBundle 进行援用,这样在内存中就只有一份 Shader,其它用到这个 Shader 的时候就间接援用它,而不须要屡次进行 Shader.Parse 了。
第二层意思是同一个 Shader 屡次地加载卸载,没有缓存住导致的。假如 AssetBundle 进行了被动打包,生成了公共的 AssetBundle,这样在内存中只有这一份 Shader,然而因为这个 Shader 加载完后(也就是 Shader.Parse)没有进行缓存,用完马上被卸载了。下次再用到这个 Shader 的时候,内存里没有这个 Shader 了,那就必须再从新加载进来,这样同样的一个 Shader 加载解析了屡次,就造成了屡次的 Shader.Parse。一般而言,通过变体优化当前的开发者本人写的 Shader 内存占用都不高,能够对立在游戏开始时加载并缓存。
特地地,对于 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 模式下的资源管理页面中能够排查激活暗藏操作较频繁的资源,从而排查和优化相干逻辑和调用。
8. 逻辑代码
逻辑代码的 CPU 耗时优化更多是联合我的项目理论需要、考验程序员自己的过程,很难定量定性进行探讨。不过 UWA SDK 中提供了不便开发者在逻辑代码中进行打点的 API&UWA GOT Online,从而将简单的函数拆解开,在报告中排查堆栈耗时、更疾速地验证优化成果。
咱们发现有越来越的团队在应用 JobSystem 将主线程中的局部逻辑代码放入子线程中来进行解决,对于能够并行运算的逻辑,十分举荐将其放入到子线程中来解决,这样能够无效升高主线程 CPU 解决逻辑运算的压力。
9. Lua
GOT Online Lua 模式提供的剖析 Lua 造成的 CPU 耗时工具可视化水平高,堆栈清晰明了,还提供了实用且特色的倒序调用剖析性能。以下联合一个 Lua 报告 Demo 简略介绍应用该工具剖析 Lua 耗时的办法。
重申:Lua 报告中呈现的函数名称格局为:函数名称 @文件名:行号。
能够通过报告提供的 Lua 文件名 / 行号 / 函数名来定位 CPU 耗时的瓶颈函数和 CPU 耗时峰值的具体起因。Lua 函数的命名格局为 X@Y:Z,其中 X 是其函数名,在无奈获取时,X 会变为默认的 unknown;Y 是该函数定义的文件地位;Z 则是该函数被定义的行号。须要留神的是,当 Lua 脚本以字节码运行时,该值将始终为 0,因而倡议在测试时尽可能应用 Lua 源码来运行。
(1)正序调用剖析——总表(曲线图 + 列表)
曲线图:
曲线选取了选取总体 Lua 代码耗时和依照耗时均值正向排序的前五个函数耗时组成耗时曲线图,每一个数据点代表了该函数在以后帧(横坐标)的耗时(纵坐标),有助于定位耗时瓶颈函数。
列表:
列表默认依照耗时均值从高到低对 Lua 函数进行了排序,粗略展现了函数名、总 CPU 耗时、场景 CPU 耗时、耗时均值等数据。通过点击函数,能够进入对应的单个函数剖析页面。
(2)正序调用剖析——单个函数页(截图 + 曲线图 + 堆栈信息)
截图:
我的项目运行时截图与使用者选中的帧大抵对应,有助于定位问题。
曲线图:
曲线图包含了 CPU 耗时曲线图和调用次数曲线图;也能够应用下方条缩放曲线察看部分耗时状况。
从曲线图中能够察看到:函数是否存在持续性高耗时;函数是否存在短暂的大量耗时,导致卡顿;某些函数单次耗时并不高,但因为被大量的调用,导致函数总耗时较高。
函数 XXXX 堆栈信息(列表):
其中,能够在右上角选定列表数据的工夫范畴:总体堆栈信息时,工夫范畴为全副测试工夫;指定场景堆栈信息时,工夫范畴为指定场景的开启工夫;指定帧堆栈信息时,工夫范畴为以后在曲线图中选中的指定帧。
列表中各项指标含意是:总体占比,以根节点函数的总耗时为 100%,以后节点函数总耗时绝对根节点函数的总耗时占比;本身占比,以根节点函数的总耗时为 100%,以后节点函数本身耗时绝对根节点函数的总耗时占比;总耗时,工夫范畴内执行该函数的耗时;本身耗时,工夫范畴内去除子节点函数(该函数调用的函数)耗时残余的耗时;调用次数,工夫范畴内该函数被调用的次数;单次耗时,总耗时 / 调用次数,示意每次执行该函数的均匀耗时;显著调用帧数,该函数本身耗时大于 3ms 的帧数。
(3)倒序调用剖析——总表(曲线图 + 列表)
曲线图:与正序调用剖析不同的是,选取了本身耗时正向排序的前五个函数,每一个数据点代表了该函数在以后帧(横坐标)的本身耗时(纵坐标)。
列表:与上同理。
(4)倒序调用剖析——单个函数页(截图 + 曲线图 + 堆栈信息)
函数 XXXX 堆栈信息(列表):
各项指标含意(与正序相比有所不同)变为了:本身占比,以选定函数的本身耗时总和为 100%,这条调用门路下选定函数的本身耗时绝对选定节点函数总本身耗时的占比;本身耗时,工夫范畴内,这条调用门路下,选定函数本身耗时的总和;调用次数,这条调用门路的调用次数;单次耗时,代表这条路调用门路下,选定函数的均匀耗时。
在通过以上界面定位到本身耗时较高的函数后,常见的优化伎俩有:优化该函数的函数体,缩小该函数本身的耗时;定位调用次数较多的调用门路,缩小调用次数。
(5)注意事项
Lua CPU 耗时中暂不包含 GC 耗时;Lua 函数耗时相当于在进出函数时打点,统计耗时。所以如果 Lua 脚本运行时调用了 C#函数,这部分 C#函数是会被统计进去的,所以须要关注和 C# 交叉调用的状况,尽量管制在 50 次以内。
本文内容就介绍到这里啦,更多内容能够返回 UWA 学堂进行浏览。课程将从内存、CPU、GPU 三个维度探讨以后游戏我的项目中经常出现的一些性能问题。