Unity文档——优化UI控件

优化UI控件

版本检查:2017.3

-

难度:高级

 

Optimizing Unity UI指南的这一部分重点介绍特定于某些类型的UI控件的问题。虽然大多数UI控件在性能方面都相对类似,但两个突出显示是接近可发布状态的游戏中遇到的许多性能问题的原因。

UI文字

Unity的内置Text组件是在UI中显示栅格化文本字形的便捷方式。但是,有许多行为不为人所知,但经常被视为性能热点。在向UI添加文本时,请始终记住文本字形实际上呈现为单个四边形,每个字符一个。这些四边形往往在字形周围有大量空白空间,具体取决于其形状,并且很容易定位文本,使其无意中破坏其他UI元素的批处理。

文本网格重建

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

对于显示大量文本标签的任何UI,此行为都存在问题,最常见的是排行榜或统计屏幕。隐藏和显示Unity UI的最常用方法是启用/禁用包含UI的GameObject,具有大量文本组件的UI通常会在显示时导致不希望的帧速率打嗝。

有关此问题的潜在解决方法,请参阅下一章中的“ 禁用画布”部分。

动态字体和字体图集

当完整可显示字符集非常大或在运行时之前未知时,动态字体是显示文本的便捷方式。在Unity的实现中,这些字体在运行时根据UI Text组件中遇到的字符构建字形图集。

加载的每个不同的Font对象将保持其自己的纹理图集,即使它与另一种字体在同一字体系列中。例如,在一个控件上使用带有粗体文本的Arial,而在另一个控件上使用Arial Bold将产生相同的输出,但Unity将保留两个不同的纹理图集 - 一个用于Arial,一个用于Arial Bold。

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

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

每当具有动态字体的UI Text对象遇到尚未光栅化为字体纹理图集的字形时,必须重建字体的纹理图集。如果新字形适合当前地图集,则会添加该字形并将地图集重新上传到图形设备。但是,如果当前的图集太小,则系统会尝试重建图集。它分两个阶段完成。

首先,仅使用当前由活动UI文本组件显示的字形重建相同大小的图集。这包括UI Text组件,其父Canvases已启用,但已禁用Canvas Renderers。如果系统成功将所有当前使用的字形拟合到新的图集中,则会对该图集进行光栅化,并且不会继续执行第二步。

其次,如果当前使用的一组字形不能适合与当前地图集大小相同的地图集,则通过将地图集的较短维度加倍来创建更大的地图集。例如,512x512图集扩展为512x1024图集。

由于上述算法,动态字体的图集只有在创建后才会增大。考虑到重建纹理地图集的成本,在重建期间最小化是必要的。这可以通过两种方式完成。

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

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

请注意,为每个更改的UI Text组件单独触发字体图集重建。在填充极大量的Text组件时,收集Text组件内容中的所有唯一字符并填充字体图集可能是有利的。这将确保只需重建一次字形图集,而不是每次遇到新字形时重建一次。

另请注意,当触发字体图集重建时,活动UI文本组件中当前未包含的任何字符都不会出现在新的图集中,即使它们最初由于调用而添加到图集中也是如此。Font.RequestCharactersInTexture。若要解决此限制,请订阅Font.textureRebuilt委托并查询Font.characterInfo以确保所有所需字符保持准备状态。

Font.textureRebuilt代表目前无证。这是一个单参数Unity事件。参数是重建纹理的字体。此活动的订阅者应遵循以下签名:

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

专业的字形渲染器

对于字形众所周知的情况,在每个字形之间具有相对固定的位置,编写自定义组件以显示显示这些字形的精灵显然更有利。这方面的一个例子可能是分数显示。

对于分数,可显示的字符是从众所周知的字形集(数字0-9)中绘制的,不会跨地方变化,并且彼此之间以固定的距离出现。将整数分解为数字并显示适当的数字精灵是相对微不足道的。这种专门的数字显示系统可以以无分配的方式构建,并且比Canvas驱动的UI Text组件更快地计算,动画和显示。

后备字体和内存使用情况

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

但是,为了支持此行为,Unity会将“字体名称”字段中列出的所有字体加载到内存中。如果字体的字符集非常大,则后备字体占用的内存量可能会过大。这在包括象形字体(例如日文汉字或汉字)时最常见。

最佳的适应性和性能

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

“最佳拟合”动态地将字体大小调整为最大整数点大小,该大小可以在文本组件的边界框中显示而不会溢出,并被限制为可配置的最小/最大点大小。但是,由于Unity为显示的每个不同大小的字符将字形图集呈现为字体图集,因此使用Best Fit会迅速压倒具有许多不同字形大小的图集。

从Unity 2017.3开始,Best Fit使用的尺寸检测不是最佳的。它为每个测试的大小增量生成字体图集中的字形,这进一步增加了生成字体图集所需的时间。它也往往导致图集溢出,导致旧的字形被踢出图集。由于Best Fit计算所需的大量测试,这通常会驱逐其他Text组件使用的字形,并在计算出适当的字体大小后强制重建字体图集至少一次。这个特定问题已在Unity 5.4中得到纠正,Best Fit不会不必要地扩展字体的纹理图集,但仍然比静态大小的文本慢得多。

频繁的字体图集重建将迅速降低运行时性能并导致内存碎片。设置为Best Fit的文本组件数量越大,此问题就越严重。

TextMeshPro文本

TextMesh Pro(TMP)是Unity现有文本组件(如Text Mesh和UI Text)的替代品。TextMesh Pro使用有符号距离场(SDF)作为其主要文本渲染管道,可以在任何点大小和分辨率下干净地呈现文本。使用一组旨在利用SDF文本渲染功能的自定义着色器,TextMesh Pro可以通过简单地更改材质属性来动态更改文本的视觉外观,以添加视觉样式,如扩张,轮廓,柔和阴影,斜角,纹理,发光等,并通过创建/使用材料预设来保存和调用这些视觉样式。

在2018.1发布之前,TextMesh Pro作为资产商店包被包含在一个项目中。自2018年起,TextMesh Pro将作为Package Manager包提供。

文本网格重建

与Unity的内置UIText组件非常相似,对组件显示的文本进行更改将触发对Canvas.SendWillRendererCanvases和Canvas.BuildBatch的调用,这可能代价高昂。最小化对TextMeshProUGUI组件的文本字段的更改,并确保其文本经常更改为具有其自己的Canvas组件的父GameObject的父TextMeshProUGUI组件,以确保Canvas重建调用保持尽可能高效。

请注意,对于世界空间中显示的文本,我们建议用户使用普通的TextMeshPro组件而不是使用TextMeshProUGUI,因为使用Worldspace中的Canvases可能效率低下。直接使用TextMeshPro会更有效,因为它不会产生画布系统开销。

字体和内存使用情况

鉴于TMP中没有动态字体功能,必须依赖后备字体。了解如何加载和使用后备字体对于在使用TMP时优化内存至关重要。

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

TextMesh Pro的字体资源在场景或项目中引用时会加载。它们主要由TextMeshPro Text组件引用,由TMP Settings引用,也由Font Assets本身引用,作为后备字体。如果在TMP设置资源中引用了字体资源,则在激活具有TMP文本组件的第一个场景时,将递归加载这些字体资源及其所有后备字体资源。如果引用了默认的精灵表资产,那么也会加载该资产。

此外,当字体资产由给定场景中的TextMeshPro组件引用并且尚未通过TMP设置加载时,一旦激活组件,将以递归方式加载引用的字体资产及其所有后备字体资产。在处理具有许多字体的项目时,请务必牢记此过程,尤其是在可用内存存在问题的情况下。

由于上述原因,在使用TMP时本地化项目成为一个问题,因为预先通过TMP设置加载所有本地化语言字体资源将不利于内存压力。如果本地化是必要的要求,我们建议一种潜在的策略,即只在必要时(当加载各种场景时)分配这些字体资产或回退,或者使用资产包以模块化方式加载字体资源。

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

  1. 为基本TMP字体资产创建资产包(例如,每种字体的最小拉丁字形)
  2. 为每种语言所需的后备TMP字体资产创建资产包(例如,日语所需的每种字体的TMP字体资产的一个资产包)
  3. 在引导步骤中加载基础Asset Bundle
  4. 根据区域设置,使用后备字体加载所需的Asset Bundle
  5. 对于基本资产包中的每种字体,从本地化字体Asset Bundle中指定回退字体资源
  6. 继续引导您的游戏

如果没有使用图像,也可以从TMP设置中删除默认精灵资产参考,以节省额外的适度内存。

最佳的适应性和性能

再一次,鉴于TextMesh Pro没有动态字体功能,上面在UGUI UIText部分中概述的有关Best Fit的问题不会发生。在TextMesh Pro组件上使用Best Fit时唯一要考虑的是二进制搜索用于查找正确的大小。使用文本自动调整大小时,最好测试最长/最大文本块的最佳点大小。确定此最佳大小后,禁用给定文本对象的自动调整大小,然后在其他文本对象上手动设置此最佳点大小。这有利于提高性能并避免使用一组使用不同点大小的文本对象,这被认为是不良的视觉/印刷练习。

滚动视图

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

  • 填充表示所有滚动视图内容所需的所有元素
  • 汇集元素,根据需要重新定位元素以表示可见内容。

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

第一种解决方案需要越来越多的时间来实例化所有UI元素,因为要表示的项目数量增加,并且还增加了重建Scroll View所需的时间。如果Scroll视图中只需要少量元素,例如在Scroll视图中只需显示少量Text组件,则此方法因其简单性而受到青睐。

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

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

简单的滚动视图元素池

使用Scroll View实现对象池的最简单方法是同时保留使用Unity内置Scroll View组件的原生便利性,采用混合方法:

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

然后,实例化一个可见UI元素池,该池足以填充Scroll View可见区域的可见部分,并将这些元素父对象定位占位符。滚动视图滚动时,重复使用UI元素以显示已滚动到视图中的内容。

这将大大减少必须批量处理的UI元素的数量,因为批处理的成本仅基于Canvas中的Canvas渲染器的数量而不是Rect Transforms的数量而增加。

简单方法的问题

目前,无论何时重新定义任何UI元素或更改其兄弟顺序,该元素及其所有子元素都标记为“脏”并强制重建其Canvas。

这样做的原因是Unity没有分离回调以重新定义转换和改变其兄弟顺序。这两个事件都将触发OnTransformParentChanged回调。在Unity UI的Graphic类的源代码中(参见源代码中的Graphic.cs),实现了该回调并调用方法SetAllDirty。通过弄脏Graphic,系统确保Graphic在渲染下一帧之前重建其布局和顶点。

可以将画布分配给Scroll视图中每个元素的根RectTransform,然后将重建限制为仅重新定义的元素,而不是Scroll View的全部内容。但是,这往往会增加渲染滚动视图所需的绘制调用次数。此外,如果滚动视图中的各个元素很复杂并且由十几个图形组件组成,特别是如果每​​个元素上有大量的布局组件,则重建它们的成本通常高到足以显着降低低端设备的帧速率。

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

基于位置的滚动视图池

为了避免上述问题,可以创建一个Scroll View,通过简单地移动其包含的UI元素的RectTransforms来汇集其对象。如果不改变其尺寸,则可以避免重建移动的RectTransforms的内容,从而显着提高滚动视图的性能。

要实现此目的,通常最好编写Scroll View的自定义子类或编写自定义Layout Group组件。后者通常是更简单的解决方案,可以通过实现Unity UI的LayoutGroup抽象基类的子类来实现。

自定义布局组可以分析基础源数据以检查必须显示的数据元素数量,并可以适当调整Scroll View的内容RectTransform的大小。然后,它可以订阅Scroll View更改事件,并使用它们相应地重新定位其可见元素。

猜你喜欢

转载自blog.csdn.net/zhaoguanghui2012/article/details/81279593