Unity UGUI优化与原理【unity官方】

来源( 来源:unity官方 Optimizing Unity UI )

官方链接:

[1]  https://unity3d.com/cn/learn/tutorials/temas/best-practices/guide-optimizing-unity-ui

[2] https://unity3d.com/cn/learn/tutorials/topics/best-practices/fundamentals-unity-ui

[3] https://unity3d.com/cn/learn/tutorials/temas/best-practices/unity-ui-profiling-tools?playlist=30089

[4] https://unity3d.com/learn/tutorials/topics/best-practices/fill-rate-canvases-and-input

[5] https://unity3d.com/cn/learn/tutorials/topics/best-practices/optimizing-ui-controls

Unity 官方优化实践 https://unity3d.com/cn/learn/tutorials/s/best-practices

 

一、核心目标

  • draw calls和batching花费的平衡

二、UGUI理论基础

术语:

  •  Canvas, 是Unity渲染系统给层状几何体( layered geometry  )提供的可以被画入、被放在上面或者放在世界空间的底层Unity组件。Canvas负责将它包含的几何体组合成batch,生成合适的渲染命令发送给Unity图形系统。这个过程在底层的C++代码中完成,这个过程被称为一次rebatch或者一次batch build。当一个Canvas被标记为包含需要rebatch的几何体时,这个Canvas被认为是dirty的。
  •  layered geometry , 由Canvas Renderer组件提供给Canvas。[ Canvas 负责进行渲染, Canvas Renderer负责采集/接收. ]
  •  动静隔离 , 一个子Canvas仅仅是一个嵌套在父Canvas中的组件,子Canvas将它的子物体和它的父Canvas隔离,一个子Canvas下dirty的子物体不会触发父Canvas的rebuild,反之亦然。(这些在某些特殊情况下是不确定的,比如说改变父Canvas的大小导致子Canvas的大小改变。).
  • Graphic , 是UGUI的C#库提供的一个基类。它是UGUI所有类的基类,给所有的UGUI类提供可以画在Canvas系统上的几何图形。大多数Unity内置的继承Graphic的类都是通过继承一个叫MaskableGraphic的子类来实现,这使得他们可以通过IMaskable接口来被隐藏。Drawable类的子类主要是image和text,已经提供了同名的组件。
  • Layout , 组件控制着RectTransform的大小和位置,经常被用于要生成具有相似的大小和位置关系内容的复杂布局。它只依靠RectTransform,只影响与其相关的RectTransform的属性。这些layout组件不依赖于Graphic类,可以独立于UGUI的Graphic组件之外使用。
  • CanvasUpdateRegistry ,  Graphic和Layout组件都依赖于CanvasUpdateRegistry类,它不会在Unity编辑器的界面中显示。这个类追踪那些Graphic和Layout组件必须被更新的时候,还有与其对应的Canvas触发了willRenderCanvases事件的时候。更新Graphic类和Layout类叫做rebuild。rebuild的过程将在本文后续讨论。

渲染细节:

  • 在使用UGUI制作UI时,请牢记Canvas中所有几何体的绘制都在一个透明队列中,这就意味着由UGUI制作的几何体将从始至终伴随着alpha混合,所以从多边形栅格化的每个像素都将被采样,即使它被完全不透明的物体所覆盖。在手机设备上,这种高等级的过度绘制将迅速超过GPU填充频率的承受能力。

Batch构建过程(Canvas)

  •  Batch , 构建过程是指Canvas通过结合网格绘制它所承载的UI元素,生成适当的渲染命令发送给Unity图形流水线。Batch的结果被缓存复用,直到这个Canvas被标为dirty,当Canvas中某一个构成的网格改变的时候就会标记为dirty。
  • Canvas的网格从那些Canvas下的CnavasRenderer组件中获取,但不包含任何子Canvas。
  • 计算Batch要求按照深度排序网格,测试它们是否有重叠,共享材质等等。这个过程是多线程的,在不同的CPU架构下性能表现非常不同,特别是在手机芯片(通常CPU核心很少)和现代桌面CPU(通常拥有四核心或者更多)之间非常不同。

Rebuild过程(Graphics)

  •  Rebuild , 过程是指Layout和UGUI的C#的Graphic组件的网格被重新计算,这是在CanvasUpdateRegistry类中执行的。这是一个C#类,它的源码可以在Unity的Bitbucket上找到。
    CanvasUpdateRegistry , 类中,PerformUpdate方法,当一个Canvas组件触发它的WillRenderCanvases事件时,这个方法就会被执行。这个事件每帧调用一次。
    PerformUpdate ,  函数运行的三个步骤:
    1- 通过ICanvasElement.Rebuild函数,请求rebuild被Dirty的Layout组件。
    2- 所有被注册的裁剪组件(例如Mask),对需要被裁剪的组件进行剔除。这在ClippingRegistry.Cull中执行。
    3- dirty的Graphic组件被要求rebuild其图形元素。
  • Layout 和 Graphic 的 rebuild ,
    Layout:Layout rebuild , 三个部分 PreLayout , Layout  ,  PostLayout
    Graphic:Graphic rebuild ,两个部分 PreRender , LatePreRender
  • Layout Rebuild , 要重新计算一个或者多个Layout组件所包含的UI组件的适当位置(以及可能的大小),有必要对Layout应用层次的排序。在GameObject的hierarchy中靠近root的Layout可能会影响改变嵌套在它里面的其他Layout的位置和大小,所以必须首先计算。 为此,UGUI根据层次结构中的深度对dirty的Layout组件列表进行排序。层次结构中较高的Layout(即拥有较少的父transform)将被移到列表的前面。然后,排序好的Layout组件的列表将被rebuild,在这个步骤Layout组件控制的UI元素的位置和大小将被实际改变。关于独立的UI元素如何受Layout组件影响的详细细节,请参阅Unity Manual的UI Auto Layout章节。 [ 这就是为什么unity的布局组件一旦形成嵌套,套内组件将失效的原因 , unity也暂时未开放布局执行层级顺序的接口 , 仅在UGUI代码中可见但不未公开 ]
  • GraphicRebuild , 当Graphic组件被rebuild的时候,UGUI将控制传递给ICanvasElement接口的Rebuild方法。Graphic执行了这一步,并在rebuild过程中的PreRender阶段运行了两个不同的rebuild步骤:1.如果顶点数据已经被标为Dirty(例如组件的RectTransform已经改变大小),则重建网格。2.如果材质数据已经被标为Dirty(例如组件的material或者texture已经被改变),则关联的Canvas Renderer的材质将被更新。Graphic的Rebuild不会按照Graphic组件的特殊顺序进行,也不会进行任何的排序操作。
  • 便于理解,制作了相关关系的导图 

三、UGUI性能分析工具

  • Unity Profiler

Unity Profiler的主要用途是执行性能比较分析:当Unity Profiler运行的时候进行enabling和disabling的操作,它可以迅速的缩小定位到性能问题最大的UI层级。查看profiler输出结果的“Canvas.BuildBatch”和“Canvas.SendWillRenderCanvases”。               Canvas.BuildBatch是执行Canvas的Batch build过程的底层代码计算量。Canvas.SendWillRenderCanvases包含了C#脚本对Canvas组件的willRenderCanvases事件的订阅的调用。UGUI的CanvasUpdateRegistry类接收这个事件并且通过它来执行前文所描述的rebuild过程。预计所有被标dirty的UI组件都会在这个时候更新他们的Canvas Renderer。

注意:为了更容易地看到UI性能的差异,通常建议禁用除了Rendering和Scripts以外所有trace category。这可以通过点击CPU Usage profiler左侧的名叫trace category旁边的彩色方块来完成。

还要注意,category可以在CPU profiler中重新排列,可以点击或者拖拽category向上或者向下来对他们进行重新排列。

  • Unity Frame Debugger

Unity Frame Debugger是一个减少UGUI的draw call的实用工具。这个内置的工具可以通过Unity Editor中的Window菜单来访问。当它运行的时候,它将显示包括UGUI在内的所有Unity产生的draw call。特别要注意的是,Unity Frame Debugger             在Unity Editor界面就可以更新游戏视口产生的draw call信息,因此可以用来尝试不同的UI配置而无需进入游戏模式。

UGUI的drawcall产生的位置取决于Canvas组件上被设置的渲染模式:

1.Screen Space – Overlay将出现在Canvas.RenderOverlays组中。

2.Screen Space – Camera将出现在Render.TransparentGeometry子项,所选渲染相机的Camera.Render组中。

3.World Space将出现在Render.TransparentGeometry子项,每个可以看见Canvas的World Space的摄像机中。

如果UI的shader没有被自定义的shader替换的话,那么所有UI都可以被 “Shader: UI/Default”识别,列出在哪个组和drawcall的细节。在下图中请看高亮红框标注的地方。

在调整UI的时候观察Unity Frame Debugger所显示的信息,这就相对比较简单的使Canvas中的UI元素最优的合成batch。最常见的与设计相关的打断批次的原因是UI元素间不小心造成的重叠。

所有的UGUI组件将它们的几何体生成成一系列的 quad。然而,很多sprite和text只占用用于显示它们的 quad的一小部分,留下了大量的剩余空间。这样的结果就是,UI开发者无意中使多个不同的 quad互相覆盖,它们的texture来自不同的        material,不能合成batch。

由于UGUI的操作完全在透明队列中,任何有不能合batch的quad在它上边的quad必须在不能合batch的quad之前绘制,因此它不能与放在不能合batch的quad上的quad合batch。(翻译这段我尽力了,但是估计还是不清楚。我讲一下大意:就是两个能合batch的quad中间夹了一个不能合batch的quad,造成这两个quad也不能合batch了)

考虑一个情景,有三个quadA、B、C。假设这三个quad彼此覆盖,并且A和C使用了相同的Material,B使用了单独的Material。B不能和A、C合成batch。

如果在层级结构中从上到下的是A、B、C,那么A、C也不能合batch,因为B必须绘制在A的上面,C的下面。然而,如果B被放在可被合batch的quad前面或者后面,那么可以被合batch的quad就能构成batch。B只需要在batch的quad之前或者之后绘制,而不会介入其中。

关于这个问题更深入的探讨,请看Canvas章节的Child order部分。

  • Instruments & VTune

XCode的Instruments和Intel的VTune各自可以非常深入的分析UGUI的rebuild和Canvas的batch计算在Apple设备和Intel CPU上的性能。方法名称几乎和我们之前介绍过的Unity Profiler的标签完全相同。它们是:

Canvas::SendWillRenderCanvases是一个C++父类调用C#中的Canvas.SendWillRenderCanvases方法,并控制 Unity Profiler中该行显示。它包含了用于进行rebuild过程的代码,这已经在上一章节详细介绍了。

Canvas::UpdateBatches几乎和Canvas.BuildBatch完全相同,但是增加了Unity Profiler页面并不包括的代码引用。它运行上文描述的Canvas的batch建立的实际过程。

当通过IL2CPP构建一个Unity APP时,这些工具可以被用于更深入的查看C#中Canvas::SendWillRenderCanvases的编译。(注意:编译的方法的名字是近似的。)

IndexedSet_Sort和CanvasUpdateRegistry_SortLayoutList是用于排序显示在标为dirty的Layout组件被重新计算之前的一个列表。如上文所述,这包括了计算每个Layout组件的父transform数量。

ClipperRegistry.Cull调用所有IClipRegion接口注册的实现者。内置的实现者包括使用IClipRegion接口的RectMask2D组件。当ClipperRegistry.Cull被调用时,RectMask2D组件将遍历在它层级下的所有要被裁剪的UI元素,更新他们的剔除信息。

所有可嵌套元素,并要求它们更新其剔除信息。

Graphic_Rebuild包含所有要显示的Image,Text或其他Graphic派生的组件所需要的网格的实际计算性能开销。在这之下有其他一些方法,如Graphic_UpdateGeometry,最值得注意的是Text_OnPopulateMesh。

-当Best Fit勾选时,Text_OnPopulateMesh通常是一个热点。这将在本指南后面详细讨论。

-网格修饰符,比如Shadow_ModifyMesh和Outline_ModifyMesh也在这里运行。通过这些方法可以看到shadow,       outline和其他特殊效果组件的计算性能开销。

  • Xcode Frame Debugger和Intel GPA

底层的Frame Debugger对监测UI不同独立部分的batch性能开销和UI过度绘制开销非常重要

  • Xcode Frame Debugger的使用
    为了测试一个给定的UI是否过度榨取GPU资源,可以使用Xcode内置的GPU诊断工具。首先将项目配置为使用Metal或OpenGLES3,然后进行构建并打开生成的Xcode项目工程。如果Unity在OpenGLES 2下运行,则Xcode不能对Unity进行分析,因此这些技术不能用于较旧的设备。注意:在某些版本的Xcode中,为了使图形分析器工作,有必要在Build Scheme中选择适当的Graphics API。为此,请转到Xcode中的Product菜单,展开Scheme菜单项,然后选择Edit Scheme ….选择Run target并转到Options页面。更改GPU Frame Capture选项来使API适配您的工程。假设Unity工程设置了自动选择图形API,则大多数新一代的iPad将默认选择Metal。如果有疑问,请启动项目并查看Xcode中的调试日志,前面的几行应该会指出哪个渲染路径(Metal,GLES3或GLES2)正在被初始化。注意:上述调整在Xcode 7.4中应该不是必需的,但在Xcode 7.3.1和更旧的版本中仍然偶尔会被发现是必须的。在iOS设备上构建并运行项目。GPU profiler显示在Xcode的Navigator边栏中,点击FPS条目。(图请参见原网页)GPU分析器中第一个重要的是屏幕中的三个条目:“Tiler”、“Renderer”、“Device”。这些表示:

    “Tiler”是对GPU生成几何体(包括在顶点着色器中的花费时间)过程中压力的衡量。

    ——一般来讲,“Tiler”值高表明顶点着色器计算过慢或者是绘制的顶点过多。

    “Renderer”是对GPU的像素流水线压力的衡量。

    ——一般来讲,“Renderer”值高表明应用程序超过了GPU的最大填充率,或是片段着色器效率低下。

    “Device” 是GPU使用的综合衡量标准,包括“Tiler”和“Renderer”的性能分析。它通常可以被忽略,因为它大体上跟踪监测“Tiler”和“Renderer”的较高者。

    有关Xcode GPU  Profiler的更多信息,请参阅此文档(链接见原网页)。

    Xcode’s Frame Debugger可以通过点击隐藏在GPU Profiler底部的小“相机”图标来打开。在下面的屏幕截图中,通过箭头和红色框突出显示。(截图见原网页)

    暂停一下之后,Frame Debugger的摘要视图就会出现,如下所示(截图见原网页):

    在使用默认UI着色器时,假设默认UI着色器没有被自定义着色器替换,那么由UGUI系统生成的渲染几何图形的开销将显示在“UI / Default”着色器通道下。在上面的截图中可以看到这个渲染管线的默认的UI着色器是“UI / Default”。

    UGUI只产生quad,所以顶点着色器不太可能给GPU Tiler流水线产生压力。出现在这个着色器中的任何问题都应归结于填充率问题。

  • 分析分析器结果

    1- 如果Canvas.BuildBatch或Canvas :: UpdateBatches占用了过多的CPU时间,则可能的问题是单个Canvas上的Canvas Renderer组件数量过多。请参阅“Canvas”一章的“Splitting Canvases”章节。

    2- 如果GPU过度的时间花费在绘制UI上,并且frame debugger表明片段着色器流水线是瓶颈,那么应该是UI的像素填充率超过了GPU的能力,最可能的原因是UI的过渡绘制。请参考Fill-rate, Canvases and input章节的Remediating fill-rate issues部分。

    3- 如果Graphic的rebuild占用了过多的CPU,如在Canvas.SendWillRenderCanvases或者Canvas::SendWillRenderCanvases中看到了大量的CPU时间占用,那么就需要进行深层分析,应该与Graphic的rebuil过程中的一些部分有关。

    4- 如果大量的WillRenderCanvas花费在IndexedSet_Sort或是CanvasUpdateRegistry_SortLayoutList上,时间花费在对dirty的layout组件列表进行排序,那么就要考虑减少Canvas中的Layout组件数量。请在Replacing layouts with RectTransforms和Splitting Canvases部分中也许会找到补救措施。

    5- 如果过多的时间花在Text_OnPopulateMesh上,那么Text网格的生成就是罪魁祸首。请参阅Best Fit和 Disabling Canvas Renderers部分,也许会找到补救措施。并考虑Splitting Canvases中的建议,如果正在重建的大部分文本实际上并未更改其基础字符串数据,text大量rebuild实际上并没有改变其基础的字符串数据。

    6- 如果时间花在内置的Shadow_ModifyMesh或Outline_ModifyMesh(或任何其他使用的ModifyMesh),则问题在于花费在计算修饰性网格过多的时间。考虑删除这些组件,并通过静态图像实现其视觉效果。

    7- 如果Canvas.SendWillRenderCanvas中没有特定的热点,或者它看起来每帧都在运行,那么问题可能是动态元素与静态元素混合在一起,致使整个Canvas过于频繁地重建。参见Splitting Canvases部分。

四、UGUI优化 : 填充率、Canvas和输入

修复填充率问题:

对于减轻GPU片段流水线上的压力有两种行动方案:

1.减少片段着色器的复杂性。

——有关更多详细信息,请参阅“UI着色器和低规格设备”部分

2.减少必须采样的像素数量。

由于UI着色器通常是标准化的,最常见的问题就是填充率的过度使用。导致这个问题最普遍的原因是UI元素大量重叠,或是有多个UI元素占据大部分的屏幕。这些问题都会导致高等级的过度绘制。

  • 消除看不见的UI

     简单的禁用玩家看不见的元素是对现有UI元素重新设计要求最小的方法,对于这种方法最常见的情况是打开了一个具有不透明背景的全屏UI。此时,在全屏UI下的任何UI元素都可以被禁用。最简单的方法是禁用根GameObject或是包含UI元素的GameObject。有关替代解决方案,请参阅Disabling Canvas Renderers部分。

  • 禁用不可见的摄像机输出

    如果在UGUI中打开了一个拥有不透明背景的全屏UI,world-space摄像机仍然会对在UI后面的独立的3D 场景进行渲染。渲染器并不知道全屏UGUI会遮挡整个3D场景。因此如果打开了一个不透明的全屏UI,禁用任何或者是全部的world-space摄像机将减少渲染3D世界的无用工作,从而减少 GPU的压力。注意:如果Canvas被设置为Screen Space – Overlay,不管场景中可用的摄像机有多少,Canvas都将被绘制。

  • 大部分被遮挡的摄像机

许多“全屏”UI并不实际上遮挡整个3D世界,而是留下了一个小的部分可以看到3D世界。在这种情况下,使用一个渲染的纹理来拍摄这部分3D世界可能更为理想。如果这部分可见的3D世界被缓存在渲染纹理中,那么实际的world-space摄像机就可以被禁用,此时被缓存的渲染纹理就作为3D               世界的冒充版本显示在UI屏幕后面。

  • 基于构图的UI

    在设计者中,基于构图来合并与层叠独立的背景与UI元素来构成最终的UI是非常普遍的。虽然这样做相对简单,而且易于迭代,但是由于UGUI使用的是透明渲染队列,所以无法高效工作。

    考虑一个简单的UI,有一个背景、一个按钮和一些文字在按钮上。在像素显示文字的情况下,GPU必须先采样背景纹理,然后是按钮的纹理,最后是字体的纹理,这三层全部都要采样。当UI的复杂性增加时,更多装饰性的元素将被层叠在背景之上,需要采样的数量将迅速增加。

    如果发现一个大的UI被填充率所束缚,最好的解决方案就是创建一个单独的UI Sprite,它融合了许多装饰性的或者是不变的UI元素在它的背景纹理之上。这样做减少了为了达到设计目的而必须重叠防止的元素数量,但是这样做也耗费劳动力并且也增加了项目图集的大小。

    这种将创建给定UI的需要重叠的元素合并到特定的UI Sprite上的做法也适用于子元素。考虑一个商店UI带有产品滚动的窗格,每个产品UI元素有一个边框、一个背景和一些图标来表示价格、名字和其他信息。

    这个商店UI需要一个背景,但是由于产品要在背景上滑动,产品UI元素无法融合到商店UI的背景纹理之上。然而,边框、价格、名字和产品UI元素的其他元素可以融合到产品的背景上。根据图标的大小和数量,填充率的节省相当可观。

    合并分层元素有一些缺点。特殊的元素不能再重复利用,这就需要额外的艺术家人力资源来创建。增加大的新纹理可能会显著增加需要来存储UI纹理的内存数量,特别是UI纹理未能按需求加载和卸载的情况下。

  • UI着色器和低规格设备

    UGUI使用的内置着色器包含了对隐藏、裁剪和许多其他复杂操作的支持。由于这种复杂性的增加,在iPhone4这种较低端设备上,UI着色器的表现较简单的Unity2D着色器相比表现较差。

    如果一个针对低端设备的应用程序不需要隐藏、裁剪和其他奇特的功能,那么就可以创建一个自定义的着色器来省略没有使用的操作,比如下面这个最简单的UI着色器:(着色器代码见原网页) [https://unity3d.com/learn/tutorials/topics/best-practices/fill-rate-canvases-and-input]

  • UI Canvas rebuild

    要显示任何UI,UI系统必须要为显示在屏幕上的每个UI组件构建几何体。这包括了运行动态布局代码,生成多边形来变现UI文本中字符串的字符,还有融合尽可能多的几何体到单个网格中来最小化draw call。这个过程有很多步骤,在本指南开始的基础基础概念部分有详细介绍。

    Canvas rebuild成为性能问题有两个主要原因:

    1.如果一个Canvas上有大量要绘制的UI元素,那么计算batch本身就变的非常昂贵。这是因为在排列和分析这些元素上的花费比在Canvas上绘制这些UI元素的增长更多。

    2.如果Canvas的dirty特别频繁,那么就有可能花费更多的时间在刷新一个Canvas相对较小的改变上。

    随着一个Canvas上元素数量的增加,上面两个问题会越来越严重。

    重要提示:在给定的Canvas上任何要绘制的UI元素改变,这个Canvas必须重新进行batch的build过程。该过程重新分析Canvas上的每个可绘制UI元素,而不管它是否已经改变。请注意,“改变”是指影响UI对象外观的任何改变,包括Sprite Renderer中指定的Sprite、transform的position和scale变化、包含在文本网格中的文本等等。

  • 子物体排序

    UGUI的建立是从后至前的,子对象在层级中的排序决定了它们的建立顺序。在层级循序中靠前的物体将被建立在层级顺序中靠后物体的后面。batch的build是从层级顺序的上走到下,并收集具有相同材质的游戏物体,即有相同纹理且没有中间层的对象(“中间层”是具有不同材质的图形对      象,其边界框与另外可合batch的对象重叠,并放置在两个可batch对象之间的层次结构中)。中间层的存在导致batch被打断。

    正如Unity Frame Debugger部分所述, Frame Debugger可以用来检查中间层的UI。就是上述这种情况,一个要绘制的对象插入到另外两个要绘制的原本可batch的对象之间。

    这个问题最为常发生于当text和sprite位于彼此靠近时:text的边界框可能不可见地重叠附近的sprite,因为text字形的多边形大多数都是透明的。这个问题可以通过两种方式解决:

    1.对要绘制的对象进行重新排序,以确保两个可以合batch的对象不会被不能合batch的对象打破。也就是说,移动不可合batch的对象到可合batch的对象的上方或者下方。

    2.调整各个对象的位置来消除不可见空间的重叠。

    上述两个操作都可以在Unity Frame Debugger打开并可用的情况下在Untiy Editor中执行。通过简单地观察Unity Frame Debugger中可见的drawcall次数,就可以找到一个最合适的顺序和位置来使由于UI元素重叠而导致的drawcall浪费减少到最小。

  • 拆分Canvas

    除了一些特殊的情况,将Canvas拆分通常是一个好主意。可以将元素移动到子Canvas或者是同级Canvas中。

    同级Canvas最常适用于UI中的某一部分必须与其他部分区分绘制深度,经常在其他层的上面或者下面。(例如教程中的箭头)

    在其他大多数情况下,子Canvas可以更方便的从父Canvas继承显示设置。

    乍看之下,将整个UI拆分为多个子Canvas是一种最佳做法,但要知道,Canvas系统也不会在分离的Canvas之间合成batch。高性能的UI设计要求在最小化rebuild和最小化drawcall浪费中取得一个平衡。

  • 一般准则

    由于Canvas的rebatch过程在任何时候都会包含所有要绘制的子组件的改变,所以最好将那些不是特殊情况的Canvas拆分成至少两部分。另外,如果一些元素可能会同时改变,最好将他们放到同一个Canvas中。比如这里有一个进度条和一个倒数计时器,它们俩依赖同样的底层数据,并且将同时被更新,所以它们应该被放在同一个Canvas上。

    在一个Canvas上,放置所有静态的不改变的元素,比如背景和标签。当Canvas一开始显示时它们将会被batch一次,然后它们就不会再需要被rebatch了。

    在第二个Canvas上,放置所有的动态的、频繁变化的元素。这个Canvas主要是用来rebatch被标为dirty的元素的。如果动态元素的数量变得非常多,那就要对所有动态元素进行更细的拆分,一些是经常会改变的(例如进度条、计时器显示、所有动画),还有一些是偶尔改变的。

    事实上这些在实际使用中是非常困难的,尤其是将UI控件封装成prefab的时候。许多UI转而选择拆分Canvas,将更消耗性能的控件拆分到子Canvas上。

  • Raycast优化

    Graphic Raycaster是一个相对直接的实现,它迭代所有Raycast Target设为true的Graphic组件。对于每一个Raycast Target设为true的Graphic组件,Graphic Raycaster会执行一系列测试。如果该组件通过了所有测试,则会被添加到命中的列表中。

    Raycast实现细节

    上述的测试是:

    1.如果检测到的目标的GameObject是激活的、UI组件是可用的,那就绘制(即具有几何体)。

    2.如果输入点在被检测到的UI元素的RectTransform范围内。

    3.如果被检测到的目标拥有,或者其任意深度的子物体拥有任何实现ICanvasRaycastFilter的组件,并且这个组件允许进行射线检测。

    接着检测目标列表会对元素按照深度排序,调整顺序不对的目标,并确认要在摄像机后面渲染的元素(即在屏幕中不可见)被移除了。

    如果3D或者2D的物理系统各自的Graphic Raycaster的“Blocking Objects”属性被标记,那么Graphic Raycaster也会向它们投射射线(在脚本中,该属性被命名为blockingObjects)。

    如果3D或者2D的的“Blocking Objects”被启动,那么任何绘制在一个射线遮挡物理层上的2D或是3D物体下的被检测到的目标将会被从列表中移除。

    返回最终的列表。

  • 射线优化技巧

    鉴于所有射线检测目标都必须由Graphic Raycaster进行测试,因此最好的做法是仅在必须接收点击事件的UI组件上启用“Raycast Target”设置。检测目标列表越小,必须遍历的层级越浅,每次射线检测的速度越快。

    对于那些有多个必须对点击事件响应的UI物体的复合UI控件,比如一个按钮它希望同时改变它的Text和背景颜色,这种情况一般最好在复合UI控件的根物体上设置一个单独的检测目标。当单个的检测目标接收到了点击事件,那么它可以将这个事件发送给这个复合控件中要响应的组件。

  • 层级深度与raycast filter

     当寻找raycast filter的时候,每个Graphic Raycast都会对根物体层级进行从头至尾的遍历。这个操作的性能消耗与层级的深度呈线性增长关系。层级中所有拥有Transform的组件必须经过检查,看它们是否实现了ICanvasRaycastFilter,所以这个操作的性能耗费并不廉价。

    有一些独立的UGUI组件实现了ICanvasRaycastFilter,比如CanvasGroup, Image, Mask和RectMask2D,所以这个遍历不会简单的结束。

  • 子Canvas和OverrideSorting属性

    子Canvas中的OverrideSorting属性将会造成Graphic Raycast测试停止遍历Transform层级。如果启动它不会带来排序或者射线检测的问题,那么就应该使用它来降低射线进行层级遍历的性能成本

五、其他 : 

其他UI优化技术和提示

很多时候并没有一个简洁的方法来优化UI。本章节包含了一些可能会提高UI性能的建议,但是一些是在结构上不简洁的,或是难于维护,或是有一些不好的边际效应。其他的是一些使UI初始开发变成简单的行为的解决方案,但是也会更容易造成一些性能问题。

基于RectTransform的布局

Layout组件性能消耗相对昂贵,因为它们必须在其每次标记dirty时重新计算其子元素的大小和位置(有关详细信息,请参阅Fundamentals章节的Graphic rebuild部分)。如果给定Layout中的元素数量相对较少且数量固定,并且Layout结构相对简单,则可以使用基于RectTransform的Layout替换Layout。

通过分配一个RectTransform的锚点,RectTransform的位置和大小就能基于其父级进行缩放。例如,两个RectTransform就能实现一个简单的两列的布局:

左列的锚点应该是X:(0,0.5)和Y:(0,1)(覆盖左边屏幕)

右列的锚点应该是X:(0.5,1)和Y:(0,1)(覆盖右边屏幕)

RectTransform的大小和位置的计算将由Transform系统本身在本机代码中驱动。这通常比依靠Layout系统更高效。编写基于RectTransform的布局的MonoBehaviour脚本也是可以的。但是,这是一项相对复杂的工作,也超出了本指南的范围。

禁用Canvas Renderer

当显示或隐藏一个UI分立的部分时,通常会启用或者禁用这个UI的根游戏物体。这确保了在这个禁用的UI中没有组件接收输入或是执行Unity的回调函数。

然而,这也会导致Canvas抛弃其VBO(顶点缓冲对象)数据。重新启用Canvas会使Canvas(包括所有的子Canvas)强制进行rebuild和rebatch进程。如果这种情况发生的非常频繁,增加的CPU使用会造成应用程序的帧率卡顿。

一种可能的方法是将UI的显示和隐藏控制在其Canvas和子Canvas中,仅仅是启用或者禁用关联到Canvas或是子Canvas的Canvas Renderer组件(并不是指真的Canvas Renderer组件,而是指依赖Canvas Renderer组件的image,text等组件)。

这将UI的网格就不会被绘制,它们将会保持驻留在内存中,它们的原始batch将会被保存。此外,在UI的层级中将不会有OnEnable或是OnDisable回调函数执行。

但请注意,这样的方法将不会消除GraphicRegistry中UI的图形,所以它们仍然会出现在Graphic Raycast要检查的组件列表中。这种方法不会禁用任何隐藏UI中的MonoBehaviour脚本,所以这些MonoBehaviour脚本将会接收Unity的生命周期回调,比如说Update。

为了避免这个问题,将以这种方式禁用的UI上的MonoBehaviour脚本不应该直接来实现Unity的生命周期回调函数,而是应该通过挂载到UI根游戏物体上的“Callback Manager” MonoBehaviour脚本来接收回调函数。每当UI被显示和隐藏的时候,就会通知Callback Manager,这会确保生命周期事件根据需要传播或是不传播。关于Callback Manager模式的进一步解释超越了本指南的范围。

分配事件摄像机

如果使用Unity的内置Input Manager并且Canvas的渲染模式设置为World Space或是Screen Space – Camera模式,始终分别的设置事件摄像机和渲染摄像机非常重要。在脚本中,它始终作为worldCamera属性公开。

如果这个属性没有设置,那么UGUI将会在挂有摄像机的游戏物体中通过主摄像机标签来寻找主摄像机,至少每个World Space或是Camera Space的Canvas都会发生这种查找。GameObject.FindWithTag众所周知非常慢,所以强烈建议所有的World Space和Camera Space的Canvas都在设计时或初始化时设置其Camera属性。

这个问题不会发生在Overlay的Canvas上。

发布了42 篇原创文章 · 获赞 23 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/gaojinjingg/article/details/103565840