Optimizing Unity UI(五):Optimizing UI Controls

版本检查: 2017.3-难度: 高级

优化UnityUI指南的本节重点介绍特定于某些类型的UI控件的问题。虽然大多数UI控件在性能方面是相对相似的,但两者都是在接近可发运状态的游戏中遇到的许多性能问题的原因。

UI text

Unity的内置文本组件是在UI中显示栅格化文本符号的一种方便的方法。然而,有许多行为是不常见的,但经常出现作为性能热点。当向UI添加文本时,请始终记住,文本符号实际上是以单个四边形的形式呈现的,每个字符一个。这些四边形往往在字形周围有大量的空间,这取决于它的形状,而且很容易将文本定位成这样一种方式,以至于它无意中破坏了其他UI元素的批处理。

Text mesh rebuilds

一个主要问题是UI文本网格的重建。无论何时更改UI文本组件,文本组件都必须重新计算用于显示实际文本的多边形。如果只禁用并重新启用文本组件或其父游戏对象中的任何一个,则此重新计算也会发生,而不会更改文本。

对于任何显示大量文本标签的UI来说,这种行为都是有问题的,最常见的是leaderboards或统计屏幕。由于隐藏和显示统一UI的最常见方法是启用/禁用包含UI的GameObject,因此,具有大量文本组件的UI通常会在显示时引起不希望的帧速率问题。

有关此问题的潜在解决方法,请参阅下一章中的 Disabling Canvases 部分。

Dynamic fonts and font atlases

当完整的可显示字符集非常大或运行时不知道时,动态字体是显示文本的一种方便方法。在Unity的实现中,这些字体根据UI文本组件中遇到的字符在运行时构建一个字形Atlas。

每个不同的字体对象加载将保持自己的纹理图集(Texture Atlas),即使它是在同一字体系列的另一种字体。例如,在一个控件上使用带有粗体文本的Arial,而在另一个控件上使用ArialBold,将产生相同的输出,但是Unity将维护两个不同的纹理地图集-一个用于Arial,另一个用于ArialBold。

从性能的角度来看,最重要的是UnityUI的动态字体在字体纹理图集中为每个不同的大小、样式和字符组合保留一个字形。也就是说,如果UI包含两个文本组件,两个组件都显示字母“A”,则:

  • 如果两个文本组件的大小相同,则字体图集中将包含一个字形。
  • 如果两个文本组件不共享相同的大小(例如,一个是16点,另一个24点),则字体图谱将包含两个不同大小的字母“A”的副本。

  • 如果一个文本组件是粗体,而另一个则不是,那么字体图谱将包含粗体‘A’和普通‘A’。

每当具有动态字体的UI文本对象遇到尚未被光栅化到字体的纹理映射中的字形时,必须重建字体的纹理图谱。如果新的字形适合于当前的地图集,则将其添加,并且将地图集重新上载到图形设备。但是,如果当前的地图集太小,则系统尝试重建atlas。它在两个阶段做到这一点。

首先,atlas以相同的大小重建,只使用当前由Active UI文本组件显示的符号。包括启用父画布但已禁用画布渲染器的UI文本组件。如果该系统将所有目前正在使用的符号安装到一个新的atlas中,它就会对atlas进行栅格化,而不会继续到第二步。

第二,如果目前正在使用的atlas不能与当前atlas相同大小的atlas,则通过将atlas的较短尺寸加倍,可以创建一个更大的atlas。例如,512x512atlas扩展为512x1024atlas。

由于上述算法,动态字体的atlas只有在创建一次之后才会增长。考虑到重建纹理atlases的成本,在重建过程中必须尽量减少。这可以通过两种方式完成。

尽可能使用非动态字体,并对所需的字形集进行预配置支持。这通常适用于使用约束良好的字符集的UI,例如只使用拉丁语/ASCII字符,而且大小范围很小。

如果必须支持非常大范围的字符,例如整个Unicode集,则字体必须设置为Dynamic。为了避免可预测的性能问题,在启动时将字体的字形图集设置为一组合适的字符 Font.RequestCharactersInTexture.

请注意,字体atlas重建是个别触发的每个UI文本组件被改变。当填充大量的文本组件时,可以收集文本组件内容中的所有唯一字符,并以字体atlas作为首页。这将确保字形atlas只需要重建一次,而不是每次遇到新的字形时重建一次。

还请注意,当触发字体atlas重建时,当前未包含在活动UI文本组件中的任何字符都不会出现在新的atlas中,即使这些字符最初是由于调用Font.RequestCharactersInTexture.而添加到地图集中的。要解决这一限制,请订阅Font.textureRebuilt delegate and query Font.characterInfo,以确保所有所需字符保持准备状态。

Font.textureRebuild委托当前未记录。这是一个论点UnityEvent。参数是其纹理被重建的字体。此事件的订阅者应遵循以下签名:

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

Specialized glyph renderers 专用字形渲染器

在象形文字是众所周知的情况下,在每个字形之间有相对固定的位置,写一个自定义组件来显示这些符号的精灵是非常有利的。这方面的一个例子可能是一个分数显示。

对于分数,可显示的字符从一个已知的字形集(数字0-9)中画出来,不在各地变化,并以固定的距离出现。将整数分解成其数字并显示适当的数字闪烁是相对简单的。这种专门的数字显示系统可以以既可分配又比画布驱动的UI文本组件更快地计算、动画和显示的方式来构建。

Fallback fonts and memory usage

对于必须支持大型字符集的应用程序,在字体导入器的“字体名称”字段中列出大量字体是很有诱惑力的。如果字形不能位于主字体中,“字体名称”字段中列出的任何字体都将用作回退。回退顺序由字体在“字体名称”字段中列出的顺序决定。

但是,为了支持这种行为,Unity将保留加载到内存中的“字体名称”中列出的所有字体。如果字体的字符集非常大,则回退字体所消耗的内存量可能过多。当包括象形字体(如日本汉字或汉字)时,经常会看到这一点。

Best Fit and performance

In general, the UI Text component's Best Fit setting should never be used.

“BestFit”(最佳拟合)动态调整字体的大小,使其达到最大整数点大小,该大小可以在文本组件的边界框中显示,而不会溢出,夹紧到可配置的最小/最大点大小。但是,由于Unity会将不同的图示符呈现到字体图集中,因此使用“BestFit”会快速覆盖具有许多不同字形大小的图集。

从Unity 2017.3开始,最佳匹配所使用的尺寸检测是非最优的。它在字体atlas中为测试的每个大小增量生成字形,这进一步增加了生成字体atlas所需的时间。它还会导致atlas溢出,从而导致旧图形被踢出atlas。由于最佳匹配计算所需的大量测试,这通常会消除其他文本组件正在使用的字形,并迫使字体atlas在计算适当字体大小之后至少再重新生成一次。这个特定的问题已经在Unity5.4中得到了纠正,而“最佳匹配”并不会不必要地扩展字体的纹理atlas,但是仍然比静态大小的文本慢得多。

频繁的字体atlas重建会迅速降低运行时性能,并导致内存碎片。设置为最佳匹配的文本组件数量越多,问题就越严重。

TextMeshPro Text

TextMesh Pro(TMP)是Unity 现有文本组件(如文本网格和UI文本)的替代品。TextMesh Pro(SDF)作为其主要文本呈现管道,从而可以在任意点、大小和分辨率上清晰地呈现文本。TextMesh Pro使用一组自定义着色器来利用SDF文本呈现的功能,通过简单地改变材料属性来添加诸如膨胀、轮廓、软阴影、斜面、纹理、发光等视觉样式,并通过创建/使用材料预置来保存和回忆这些视觉样式,从而动态地改变文本的视觉外观。

在2018.1发布之前,TextMesh Pro作为一个资产存储包含在一个人的项目中。从2018.1开始,TextMesh Pro将作为Package Manager package.

Text mesh rebuilds

就像Unity 的内置UIText组件一样,对组件显示的文本进行更改将触发对Canvas.endWillRendererCanvases和Canvas.BuildBatch的调用,这可能会很昂贵。尽量减少对TextMeshProUGUI组件的文本字段的更改,并确保父组件TextMeshProUGUI组件的文本经常更改到具有自己的画布组件的父GameObject,以确保画布重新生成调用保持尽可能高的效率。

请注意,对于在世界空间中显示的文本,我们建议用户使用正常的TextMeshPro组件,而不是使用TextMeshProGUI作为使用WorldSpace中的画布可能是低效的。如果不产生画布系统开销,使用TextMeshPro将是更有效的。

Fonts and memory usage

由于TMP中没有动态字体功能,所以必须依赖回退字体。当使用TMP时,了解如何加载和使用回退字体对于优化内存至关重要。

TMP中的字形发现是递归完成的-也就是说,当TMP字体资产中缺少字形时,TMP迭代当前分配或活动的回退字体资产列表,从列表上的第一个回退开始,并通过自己的回退。如果仍未找到字形,TMP将搜索可能分配给文本对象的任何Sprite资产,以及分配给此Sprite资产的任何回退。如果所需的字形仍未找到,则TMP将递归地搜索TMP设置文件中分配的常规回退列表,后面跟着默认的Sprite Asset。如果仍然无法找到此字形,它将搜索TMP设置中分配的默认字体资产。作为最后手段,TMP将使用并显示TMP设置文件中定义的缺失字形替换字符。

TextMesh Pro的字体资产在场景或项目中引用时被加载。它们主要由TextMeshPro文本组件、TMP设置以及字体资产本身作为后备字体引用。如果在TMP设置资产中引用Font资产,则在激活带有TMP文本组件的第一个场景时,将递归加载这些字体资产及其所有回退字体资产。如果引用默认的Sprite表资产,也将加载该资产。

此外,当在给定场景中由TextMeshPro组件引用字体资源并且尚未通过TMP设置加载时,一旦组件被激活,则所引用的字体资源及其所有回退字体资产将递归地加载。当在具有许多字体的项目上工作时,特别是如果可用的存储器是一个问题,那么重要的是要记住这个过程。

出于上述原因,当使用TMP时本地化项目成为关注,因为通过TMP设置而加载的所有本地化语言字体资产将对存储器压力有害。如果本地化是必要的要求,我们建议在必要时(如加载各种场景)或使用Asset Bundles以模块化方式加载字体资产的潜在策略。

当应用程序启动时,应该包括一个引导步骤,以验证用户的区域设置,并为每个字体资产设置字体资产回退:

  1. 为基础TMP字体资产创建Asset Bundles包(例如,每个字体的最小拉丁字符)
  2. 为每种语言所需的备用TMP字体资产创建一个Asset Bundle (例如,为日语所需的每种字体创建一个用于TMP字体的Asset Bundle)
  3. 在引导步骤中加载基本Asset Bundle
  4. 根据区域设置,使用后备(fallback)字体加载所需的Asset Bundle包

  5. 对于基本Asset Bundle中的每种字体,从本地化字体Asset Bundle中分配回退字体资产
  6. 继续引导游戏

如果不使用图像,也可以从TMP设置中删除默认的Sprite资产引用,以节省额外的内存。

Best Fit and performance

再次,如果TextMeshPro没有动态字体功能,则不会出现上述有关最佳拟合的UGUIUIText部分中概述的问题。在TextMeshPro组件上使用“Best Fit”时,唯一需要考虑的是使用二进制搜索来查找正确的大小。使用文本自动调整大小时,最好测试最长/最大文本块的最佳点大小。确定此最佳大小后,禁用给定文本对象的自动调整大小,然后在其他文本对象上手动设置此最佳点大小。这有利于提高性能并避免使用不同点大小的一组文本对象,而这些点大小被认为是不良的视觉/印刷实践。

Scroll Views

在填充率问题之后,UnityUI的Scroll视图是运行时性能问题的第二个最常见的来源。滚动视图通常需要大量的UI元素来表示其内容。填充滚动视图有两种基本方法:

  • 用表示所有滚动视图的内容所需的所有元素填充它

  • 汇集元素,根据需要重新定位它们以表示可见的内容。

这两种解决方案都有问题。

第一种解决方案需要越来越长的时间来实例化所有UI元素,因为要表示的项的数量增加了,还需要增加重建滚动视图所需的时间。如果在滚动视图中只需要少量的元素,比如在滚动视图中只需要显示少量的文本组件,那么这个方法的简单性就更好了。

第二种解决方案需要大量代码才能在当前UI和布局系统下正确实现。下文将进一步详细讨论两种可能的方法。对于任何显着复杂的滚动UI,通常需要某种类型的池方法来避免性能问题。

尽管存在这些问题,但是可以通过向Scroll View添加矩形的2D组件来改善所有的方法。此组件确保Scroll View视口之外的滚动视图元素不包含在可绘制元素的列表中,这些元素必须在重建画布时生成、排序和分析其几何形状。

Simple Scroll View element pooling

实现带有滚动视图的对象池的最简单的方法,同时还保留了使用Unity的内置滚动视图组件的本机便利性的大部分方法是采用混合方法:

要在UI中布局元素,这将允许布局系统正确计算滚动视图内容的大小,并允许滚动条正常工作,请使用带有Layout Element组件的GameObjects作为可见UI元素的“占位符”。

然后,实例化足够填充Scroll View可见区域可见部分的可见UI元素池,并将这些元素放到定位占位符中。在滚动视图时,重用UI元素来显示已滚动到视图中的内容。

这将大大减少必须批处理的UI元素的数量,因为批处理的成本只是基于画布中的画布渲染者的数量而不是RECT转换的数量而增加的。

Problems with the simple approach

当前,每当任何UI元素被重新加载或其兄弟顺序发生更改时,该元素及其所有子元素都被标记为“肮脏的”,并强制重建其画布。

这样做的原因是,Unity 并没有将修复转换和改变其兄弟关系顺序的回调分离开来。这两个事件都将触发OnTransformParentChanged回调。在UnityUI的图形类的源代码(参见源代码中的Graphic.cs)中,实现了回调并调用了SetAllDirty方法。通过dirtying the Graphic,系统确保Graphic将在下一个帧呈现之前重建其布局和顶点。

可以将画布分配给“Scroll View”中每个元素的根RectTransform,然后该元素将只将重建限制在已修复的元素上,而不是“Scroll View”的全部内容。但是,这会增加呈现Scroll View所需的绘制调用数。此外,如果Scroll View中的单个元素很复杂,并且包含十几个图形组件,特别是如果每个元素上有大量的布局组件,那么重建它们的成本通常很高,足以显著降低低端设备上的帧速率。

如果Scroll View UI元素没有可变大小,那么完全重新计算布局和顶点是不必要的。但是,要避免这种行为,需要实现基于位置更改的对象池解决方案,而不是父级或兄弟级更改。

Position-based Scroll View pools

为了避免上述问题,可以通过简单地移动包含的UI元素的RectTransform来创建一个Scroll View 对象池装它的对象。这避免了在未更改已移动RectTransform的维度的情况下重新构建其内容的需要,从而显着地提高了Scroll View的性能。

要实现这一点,通常最好要么编写自定义的滚动视图子类,要么编写自定义布局组件。后者通常是更简单的解决方案,可以通过实现UnityUI的LayoutGroup抽象基类的子类来实现。

自定义布局组可以分析基础源数据,以检查必须显示多少数据元素,并可以适当地调整Scroll View的ContentRectTransform的大小。然后,它可以订阅 Scroll View change events,并使用这些事件相应地重新定位其可见元素。

猜你喜欢

转载自blog.csdn.net/Momo_Da/article/details/93617737
今日推荐