《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三个维度探讨以后游戏我的项目中经常出现的一些性能问题。