优化UI控件 【译】

翻译自:https://unity3d.com/cn/learn/tutorials/topics/best-practices/optimizing-ui-controls?playlist=30089

这一章节关注一些特定的UI控件。大部分UI控件在性能方面是相似的,其中有两个控件在游戏快完成时,可能会遇到许多性能问题。

UI text

Unity 内置的Text组件可以很方便的将光栅化的文本符号显示到UI中。然而又很多不被知道的问题,会频繁的产生性能开销点。当你像UI中添加文本时,请时刻记住每一个文字的文本符号都是一个独立的四边形。根据形状的不同,这些四边形可能的周围可能会包围着很多透明区域,而且很容易放置text组件,导致阻断其他可以合批的UI元素。

Text 网格重建

一个很严重的问题就是UI text的重建。无论何时,当UI Text组件发生变化时,text 组件会重新计算实际显示的多边形。当一个文本的父级GameObject只是简单的disable & re-enabled,并没有改变文本时,也会导致重新计算。

这种行为对于那些显示大量文字标签的UI如排行榜和统计界面来说,是有问题的。最常见的方式去隐藏和显示Unity UI就是enable/disable一个包含了UI的GameObject,当显示包含大量Text组件的文本时,经常会出现的意外的帧率峰值。

有关此问题的解决方法,请参阅下一章的 Disabling Canvas Renderers 章节。

动态字体和字体图集

显示拥有大量可显示文字的字库,或者运行时无法预测的文本时,使用动态字体是一种非常便利的方式。在Unity中,这些字体根据UIText组件运行时使用到的字符,动态的创建一个字符图集。

每个加载的字体对象都包含一个自己的纹理图集,即使它和另一个字体有相同的font family。比如,在一个控件上使用Arial粗体,在另一个控件上使用Arial Bold字体,它们的表现是相同的,但是Unity会保留两张独立的纹理贴图——一张给Arial字体使用,一张给Arial Bold字体使用。

Unity UI的动态字体会在字体的纹理图集中,每一个大小,风格不同的文字都会保留一个字符(glyph )。这意味着,如果一个UI包含两个Text组件,两个都显示字符‘A’,那么:

  • 如果两个文本的字体大小相同,那么在字体图集中只会有一个字符(glyph)。
  • 如果两个文本的字体大小不同(如一个16-point,另一个24-point),那么字体图集会包含字符‘A’的两个不同大小的拷贝。
  • 如果一个文本是粗体,另一个不是,那么字符图集中将包含一个粗体的‘A’和一个正常的‘A’。

当一个UI Text对象遇到一个还没有被字体图集光栅化的字符,那么字体图集必须重建。如果新的字符适合放入到当前的图集,它将被加入到图集中,图集会重新传递给绘制设备。然而,如果当前的图集很小,那么系统会尝试去重建图集。它分两个阶段进行。

第一步,图集会被重建成相同大小,它只包含active的UI Text组件(1)上显示的字符。如果系统成功的将所有当前使用的字符放到一个新的图集中,那么它将光栅化这个图集,并且跳过第二步。

第二步,如果当前使用的字符集合不能被放到和当前图集相同大小的图集中,一个是图集短边长度两倍的图集将被创建。比如512 * 215图集扩展到512 * 1024大小的图集。

基于上述算法,一个被创建的动态字体图集只会在大小上改变。考虑到重建纹理图集的开销,应尽量在重建过程中减少图集。有两种方式可以实现。

只要允许,尽可能的使用非动态字体,预先设置所需支持的字符集。这适合用于使用有限字符集的UI,比如只使用Latin/ASCII字符,并且数量很小。

如果大量的文字需要被支持,比如整个Unicode字符集,那么字体必须设置为Dynamic。为了避免一些可预测的性能问题,可以通过 Font.RequestCharactersInTexture 在启动时将合适大小的字符集预先填充到字体的字符图集中。

注意,任何一个Text组件改变都会触发字体图集的重建。当需要显示大量文本组件时,最好是先收集Text组件内容的所有字符,再去填充图集。这将确保字符图集只被重建一次,而不是每加入一个新字符时就需要重建一次。

同时需要注意的是,当字体图集触发重建时,任何没有被active Text组件包含的字符都不会放到新的图集中,即使它之前通过 Font.RequestCharactersInTexture已经打入到图集中。为了解决这个限制,可以监听Font.textureRebuilt回调,查询 Font.characterInfo 确保所有需要的字符包含在图集中。

Font.textureRebuilt委托,目前还没有文档。它是一个 single-argument Unity Event。它的参数是图集被重建的字体。监听这个事件应该按照以下格式:

public void TextureRebuiltCallback(Font rebuiltFont) { /* ... */ }

专门的字符渲染器

对于一些字符都是确定的,并且字符之间的相对位置是固定的,那么实现自定义的组件去显示这些字符效率会更高。其中的一个例子就是分数显示。

对于分数,可显示的文字都在一个确定的字符集中(数字0-9),并且不改变位置,和其他字符保持固定的距离。将整数分解成数字,并用数字sprite显示的开销是微不足道的。这种专门的的数字显示系统,可以使用一种不需要分配,并且计算、动画、显示快速的方法去构建。它比Canvas驱动的UI Text组件更加高效。

后备字体(Fallback fonts)和内存占用

对于必须支持大量字符集的应用程序,在字体导入器的“Font Names”字段中列出大量字体是很诱人的。当一个字符在主字体中不能被定位到,那么“Font Names”字段列出的所有字体都有可能被用来当做后备字体。选择后备字体的顺序取决于字体在“Font Names”字段列表中的顺序。

然而,为了支持这种操作,Unity 需要将“Font Names”字段列出的所有字体都加载到内存中。如果字体集非常大,那么后备字体将会占用非常多的内存。这种问题经常在包含象形文字的字体(如日本文字和汉字)时出现。

Best Fit 和 性能

通常,UI Text组件的 Best Fit 设置不应该被使用。

“Best Fit”可以动态的调整字体的大小,让字体在没有超出边界的情况下,将字体调整到最大的整数值,还可以设置最大/最小值来限制字体大小。然而由于Unity的渲染器将所有显示的不同大小的字符单独的放入到字体图集中,使用Best Fit设置,不同大小的字符将会很快的填满图集。

对于Unity5.3版本,使用Best Fit去检测大小的算法并不是最好的。它会将每一个用于尺寸增量测试的字符都加入到字体图集中,这进一步的增加了生成字体图集所需要的时间。这也有可能导致字体移除,部分之前的字符会被移除图集。由于Best Fit 需要大量的测试计算,经常会产生别的Text组件使用的字符被移除,并且在合适的字体大小被计算出来之后,字体图集又会被强制重建至少一次。

这个问题在Unity 5.4版本中被解决了,Best Fit 不会不必要的扩展字符图集,但是它仍然比静态大小的字体慢很多。

频繁的字体重建会产生内存碎片,降低运行时的性能表现。设置Best Fit的文本组件越多,这个问题越严重。

Scroll Views

在填充率问题之后, Unity UI的Scroll View 是第二常见的运行时性能问题的来源。Scroll View通常需要大量的UI元素来当做它的内容。有两种基础的填充Scroll View的方式:

  • 将scroll view内容需要的所有元素都填充进去。
  • 缓存元素,当它们需要显示时,重新设置它们的位置。

两种方法都有问题。

第一种方法会随着需要实例化的UI元素增多,消耗更多的时间,并且会增加Scroll View重建的时间。如果ScrollView中只有少量的元素,例如在滚动视图中,只需要显示少量文本组件,那么这种方式更简洁。

第二种方法需要大量的方法去确保当前的UI和布局系统能显示正常。还有两种可行方案将在下面讲述。对于非常复杂的UI,通常使用缓存池的方法可以在一定程度上减少性能开销。

无论哪种方法,在ScrollView上添加RectMask2D组件,都可以提升性能。当Canvas重建时,这个组件可以确保Scroll View视窗外的元素不包括在必须具有生成,排序和分析其几何的可绘制元素列表中。

简单的Scroll View 元素池

最简单的方式去实现scroll view的缓存池,同时保留Unity内置Scroll View组件的便利性就是使用一种混合的方法:

在UI中布局元素,需要允许布局系统正确的计算Scroll View内容的大小,并且让滚动条运行正常。要布局的时候,可以使用挂载了Layout Element组件的GameObject为可见的UI占位。

然后,实例化一个可见元素的池,让可见元素可以填满Scroll View的可见部分,并且将它们的父级设置为占位的GameObject。当Scroll View滚动时,重用UI元素来显示已经滚动到视图中的内容。

这实质上是减少必须被合批的UI元素的数量,因为合批的开销只是依赖于Canvas中 Canvas Renderers的数量,而不是Rect Transform的数量。

这种简单方法的问题

当任何一个UI元素的父级变化或者它的sibling order发生变化,那么这个元素和它的子元素都会被设置为"dirty",并且强制它们的Canvas重建。

原因是因为Unity还没去区分父级变化和silbing order变化的回调。这两个事件都会触发OnTransformParentChanged的回调。在Unity UI的Graphic类(源码中Graphic.cs)中,那个回调会调用SetAllDirty方法。因为被设置为dirty,系统会保证这些Grapihc会在下一帧渲染之前重建它的布局和顶点。

可以为每个Scroll View中的元素的根RectTransform添加Canvas,这样只会去重建父节点变化的元素,而不是整个Scroll View的内容。然而,这种方式会增加Scroll View渲染的drawcall。此外,如果Scroll View中的单个元素很复杂,包含了非常多的Graphic组件,并且还包含了很多布局组件,那么在低端机上重建它们的开销会明显的降低帧率。

如果Scroll View的UI元素的大小不会变化, 那么没有必要对全部的组件的布局和顶点的重新计算。避免这种行为,需要实现一个机遇位置变化的而不是父级或者sibling-order变化的对象池解决方案。

基于位置的 Scroll View 缓存池

为了避免上述问题,可以创建一个Scroll View通过移动它包含UI的RectTransform来实现缓存对象。这样避免了重建那些已经移动过的并且大小没有变化的RectTransform,可以显著的提升Scroll View的性能。

通常是通过写一个自定义的Scroll View类,或者写一个自定义的Layout Group组件来实现这个操作。后者通常是更简单的解决方案,可以通过实现Unity UI的LayoutGroup抽象基类的子类来实现。

自定义布局组可以分析潜在的源数据,以检查必须显示多少个数据元素,并可以适当地调整Scroll View的内容RectTransform的大小。可以监听Scroll View change events事件,相应的调整可见元素的位置。

尾注

  1. 这包括父级的Canvas是enabled,但是Canvas Renderers disabled 的Text组件。

转载请注明出处:http://www.cnblogs.com/chiguozi/p/6803710.html

原文地址:https://www.cnblogs.com/chiguozi/p/6803710.html