【优化笔记】UGUI性能优化方案

性能检测参考函数 [CPU]

  • 1、Canvas.SendWillRenderCanvases()
    该API为UI元素自身发生变化时所产生的调用,发生在canvas被渲染之前。

  • 2、Canvas.BuildBatch[Cpp]
    该API为UI元素合并的Mesh需要改变时所产生的调用。通常之前所提到的Canvas.SendWillRenderCanvases()的调用都会引起Canvas.BuildBatch的调用。另外,Canvas中的UI元素发生移动也会引起Canvas.BuildBatch的调用。

    以上两种指标产生的主要来源是Graphic类型组件和Layout类型组件。
    

优化指标

DrawCall & OverDraw

在每次绘图前,都需要先准备好顶点数据(位置、法线、颜色、纹理坐标等),然后调用一系列API把它们放到GPU可以访问到的指定位置,最后,我们需要调用_glDraw命令,而调用_glDraw命令的时候,就是一次Draw Call
从API调用的角度来看,Batch和Draw call是等价的,但是在游戏引擎中他们的实际意义是不一样的:Batch一般指代经过打包之后的Draw call。
CPU的性能决定提交Batch的效率,在不给GPU造成渲染压力的前提下,Batch越大越好。
Overdraw指的是,我们可能对屏幕上的像素绘制了多次。[GPU优化]

优化目的

Batch(批处理)相关优化

  1. Canvas
    作为图像绘制的基本单位,一个Canvas必然增加一个dc,Canvas是直接造成dc数上升的因素,因此不能滥用Canvas或Sub-canvas,每个界面的canvas数要尽量控制在合理范围。

  2. 材质图集

    • 合批的必要条件之一是同一材质,因此尽量保证同时同地出现的UI元素使用同一材质,也就意味着他们需要打在同一图集上。
    • 同一界面使用到的不同图集尽量少,在打图集时按照功能界面划分,如背包界面,技能界面,人物属性等,通用元素打在同一图集。
  3. 预加载
    预加载一定程度上能避免因动态加载造成的合批被打断

  4. 隐藏看不见的UI&主相机隐藏
    不需要显示在屏幕上的UI或场景需要及时进行隐藏,这里的隐藏不是指单单设置为透明,而是让它不要被渲染到,这样能大大减少不必要的dc数,这是最简单也是最常被忽略的点。
    全屏界面下,用于渲染场景的主相机就可以关闭了,因为渲染场景产生的dc是一笔巨大开销

  5. 简化Prefab深度(复杂度)
    简化Prefab主要是为了减少合批时的排序计算时间,减少UI重建和渲染时间,另外尽量简化Prefab,功能类似的prefab进行复用,一定程度上也能减少合批数.

  6. 对于能合批的组件尽量相邻,避免中间层
    “中间层”是指带有不同材质的绘图对象,它的边界与两个可另行批处理(otherwise-batchable)对象重叠并且位于两个可批处理对象之间。 中间层会强制破坏批处理。
    每次进行合批操作时,都会根据prefab的结构从上至下进行遍历Hierarchy,将不同深度可能合批的对象进行合批,中间层过多会造成算法复杂度增加。

  7. 对于窗口类型的界面,必须显示一个场景背景
    可以考虑把可见的场景内容“缓存”到RenderTexture中,那么实际的世界空间相机就可以被禁用,然后将缓存的RenderTexture绘制到UI屏幕后方,来提供一个伪造的3D世界画面。

Rebuild(重建)优化

重建的开销主要是:

  1. UI元素被频繁标记为脏(即顶点数据变化)从而频繁重建消耗性能
  2. 由于画布下的UI元素过多且排布不规律,深度太大,而导致一次重建的时间较长,计算排序等操作消耗性能

因此优化的主要方式为:

  1. 避免频繁的OnEnable调用,或者干脆不调用。这就需要优化UI界面的隐藏显示方法。
  2. 将频繁变动的UI元素分离出去,避免其影响到其它静态的元素造成整体的重建,也就是减少被"弄脏"的可能。
  3. 简化UI结构

关于界面/控件隐藏的多种方案

   界面的隐藏方案,主要是为了针对避免SetActive时产生的GC以及其它周期函数调用等消耗,同时最好能避免 OnEnable带来的大量重建消耗。 
  1. SetActive 会产生GC,会导致画布丢弃它的VBO数据,从而重建和重新批处理,操作频繁时GC导致CPU占用可能会卡顿。
  2. enable Canvas,禁用画布,能降低dc,避免SetActive的GC开销,但测试依旧会有SendWillRenderCanvases的开销。
  3. 移出RootCanvas,似乎没有多大优化,理论上会降低dc且不会引起重建。
  4. 将单独界面使用Canvas,绑定一个Camera在隐藏时设置Layer进行CullingMask屏蔽,同时禁用GraphicsRaycaster,能避免重建,但随着canvas增多dc会增多,同时界面较多时管理比较费力,注意动态UI的事件。
  5. 设置Canvas Group 的alpha,依旧会被渲染,但能避免setactive带来的巨大开销,且容易操作。
  6. 更改Scale的值为0,顶点信息被清除,这样不会减少dc开销,同时还会引起重建,不采用。
  7. 设置一个Canvas作为根节点,将其layer设置为相机屏蔽的层级,同时禁用GraphicsRaycaster,当需要隐藏某个界面时将其以该Canvas作为根节点移到其下,这样能避免重建同时减少dc,但会有一些SetParent带来的消耗。目前采用该方案。
  8. 如果只是隐藏某个Graphic类的,可以考虑获取canvasRenderer.cull 设置为true (需要注意的是,在使用RectMask2D时,该属性会被修改,一般可在非使用RectMask2D的时候代码处理)
注意:在不适用SetActive的情况下,无法避免隐藏状态下继承自MonoBehavior中的周期函数被调用,特别是Update与Lateupdate。要避免这一问题,以这种方式实现隐藏的UI上的MonoBehaviour不应该直接实现Unity的生命周期回调,而应该去接收它们的UI根节点的自定义的“CallbackManager”的回调。当UI被显示和隐藏是,这个“CallbackManager”应该收到通知,并决定是否传播生命周期事件。   

OverDraw优化

有两种操作能够减轻GPU片元(fragment)管线的压力:

  • 降低片元着色器复杂度。
  • 降低必须进行采样的像素数量。

主要通过自定义简化shader替换内置shader来避免低端机上的性能瓶颈
由于UI着色器一般都会符合标准,所以最常见的问题是过多使用填充率。引起这种问题的最常见原因是,UI大量重叠并且/或者有多个UI元素占据屏幕的重要位置。这两种问题都能够导致极高的重绘,因此要降低多余的图像采集(填充率的过渡使用)

Fill Rate(填充率)是指显卡每帧每秒能够渲染的像素数。在每帧绘制中,如果一个像素被反复绘制的次数越多(重绘),那么它占用的资源也必然更多

  1. 避免文字交叉
    文字区域不易控制,经常造成Overdraw,如果是静态图,可以将文字坐到图片中,减少重绘,还可减少资源量。
  2. 谨慎使用透明UI元素
    透明元素同样会被渲染,但很容易被忽略造成重绘。
  3. 打开全屏不透明UI时禁用它后面的UI元素(禁用根GO或禁用画布)
  4. 对于不需要Graphic组件(附带CanvasRenderer)的及时移除掉
  5. 如果不是通用类型的图或其它适配问题,那可以考虑尽量将多张图做成整图以减少重叠。

一般原则

  1. 将UI细分到很多子画布,但要画布系统同样不会为不同画布中的元素合并批处理。设计高效UI需要在最小化重建开销和最小化无用批处理之间权衡。

  2. 因为画布会在其中的任意组件发生变化时进行重新批处理,所以最好将有用的(non-trivial)画布分成至少两部分。进一步将,最好将那些同时变化的元素放到同一画布上。例如,进度条和倒计时UI,它们都依靠相同的底层数据,因此会同时更新,所以它们应该放在同一画布上。

  3. 修改material改颜色和直接改color属性在性能消耗要权衡,修改材质会增加drawcall,修改color属性会重建,根据测试结果来选择。

  4. 优化策略通常是牺牲某一方面来提升另一方面的性能,例如几种避免重建的隐藏界面的方案,由于缓存的顶点数据不会被清除,因此必然带来内存的增加,而如果清除内存中顶点数据,又必然需要在显示时重建合批。

  5. 特效
    粒子系统的渲染和UI是独立的,仅能通过Render Order来改变两者的渲染顺序,而粒子系统的变化并不会引起UI部分的重建

通用组件的PrefabPool方案

PrefabPool是设想中最大程度复用UI部件的方案,主要是为了减少预制体复杂度,并且能统一修改标准化的部件,提升开发效率。
具体做法是,将所有可能重复使用到的Prefab动态加载,以嵌套的方式注入其它Prefab,在宿主Prefab被隐藏或要释放掉时,将嵌入的Prefab回收。
缺点是,这需要去提取那些可能被复用的部分单独成为一个prefab,prefab的粒度会加大,管理成本也会增加。

Prefab嵌套+Pool缓存机制

  1. 需要分解模板,将常用组件分拆成模板,单独使用缓存池管理

  2. 需要进行Prefab的嵌套,保证在释放UI界面时,只释放非模板部分

  3. 存在缓存时SetParent读取,不存在时生产后加载

  4. 能够作为Pool缓存的对象,需要尽量满足标准化,也就是所具备的功能一致,调用方式一致,样式尺寸大体一致

    目前实现的方式是,将UI实例的类型通过实现IPoolElement的接口附加可缓存入池的属性。

注意

  1. 设计UI时尽量使用拆分+组合的方式设计,尽量保证模板复用性
  2. 按钮类有事件注册的模板,记得在放回池子时移除事件
  3. 池子使用不被渲染Layer的Canvas进行存放

你可能感兴趣的:(Unity,UGUI,优化)