这一章节专门针对UI Controls进行优化。大部分的UI Controls都对性能表现有一定的影响。
UI Text(UI文本)
Unity内置的Text组件可以在UI显示格栅化的文本。以下是一些常需要关注的与性能相关的因素,在添加文字到UI的时候,事实上被渲染成多个多边形。文本文字作为独立的片面进行渲染,每个字符都是一个片面,这些多边形有很多空白的部分,在放置文本时很容易使其无意中破坏其他元素的批处理。
Text mesh rebuild(文本网格重建)
每次的文本变化都需要重新计算用于显示实际文本的多边形,在一个text component或者其他子物体被禁用或者重新启用的时候,也会进行重新计算。 这个过程对于显示大量文本的UI会有很大问题,比如排行版和统计面板。最常见的打开和关闭UI这些会导致意外的帧率问题。
Dynamic fonts and font atlases(动态字体和图集) 在字符集很大或者运行时字符使用不确定时,可以用动态字体来显示文本。在Unity中组件中的文本经根据使用字符进行创建图集。
每个不同Font对象将保留自己的纹理图集,即使它与另一种字体在同一字体系列中。例如,在一个控件上使用Arial文本,在另一个控件上调用这个本文使用的也是Arial Bold,但是Unity将保留两个纹理图集。 从性能分析来看,重要的是理解Unity UI为每个不同的大小、风格和字符维护一个字形在字体图集中。如果一个UI包含两个字体组件,都显示字符'A':
- 如果两个组件使用相同大小、字体图集那么它们将使用同一个字形。
- 如果两个组件使用不同的大小,那么字体图集将包含两个'A'
- 如果两个'A'一个加粗了,一个没有加粗
每当有UI Text对象遇到未光栅化的字体纹理图集时,字体纹理图集必须被重建。如果一个新的字形适合当前图集,它将被添加到图集并且图集并将重新加载到图形设备中。分两步执行。
首先,使用当前大小的图集进行重建。这包括UI Text组件的父Canvas是可用的,但是Canvas Renderers是禁用的。如果系统成功将当前使用的字形拟合到新的图集中,则会对新的图集进行光栅化,不会执行第二步。 第二步,如果当前使用的字形不能被相同大小的图集放下,将创建一个将图集较短维度双倍的新图集。
根据上述算法,动态图集只有在创建出来后才会增大。考虑到重建过程的消耗,有必要在重建期间最小化,可以通过下面两种方法:
在有良好的字符集约束的UI上,使用非动态字体和预配置所支持的图集。
如果需要对庞大字符的支持,比如整个Unicode,那么字体必须被设置为Dynamic。避免可以预见的性能问题,通过Font.RequestCharacterInTexture方法在开始的时候进行设置。
主意:每个UI Text component变化的时候都将触发图集的重新构建,当有大量的Text组件的时候,收集字体组件中的全部独特元素和主要的字体图集。这将确保字形图集只重建一次代替,每个新字形都重建。
在触发图集重建的时候,当前没有活动的UI Text组件中的字符将不会出现在新图集中,即使它们通过Font.RequestCharacterInTexture加入到了新图集。围绕着这些限制,注册到Font.textureRebuild
和查询Font.characterinfo确保全部字符都准备好了。
Font.textureRebuild
是一个单参数Unity事件,参数是texture要重建的字符。
Specialized glyph renderers(专用字形渲染器) 对于字形众所周知的情况,在每个字形之间具有相对固定的位置,编写自定义组件以显示显示这些字形的精灵显然更有利。 这方面的一个例子可能是分数显示。 对于分数,可显示的字符是从众所周知的字形集(数字0-9)中提取的,不会跨地方变化,并且彼此之间的距离固定。 将整数分解为数字并显示适当的数字精灵是相对微不足道的。 这种专门的数字显示系统可以以无分配的方式构建,并且比Canvas驱动的UI Text组件更快地计算,动画和显示。
Fallback fonts and memory usage(备用字体与内存使用) 对于需要使用大量字符的程序,在字体导入设置的"Font Name"输入框中列出大量的字体名称。任何在fonts lsit中将加载到内存中,如果首选字体中没有,将在备用字体在FontName中查找。 然而为了支持这个方法,Unity将在Font Names中的文本加载到了内存,如果字体元素很大,那么通过回调函数得到的字体将很大。这种情况经常出现在含有象形文字时。
Best Fit and performance(字体适配与性能) "Best FIt"启用后,动态适配字体的大小在最大字号与最小字号之间动态调整,可以显示在文本组件中不会超出边界。然而Unity渲染不同的字形到字体图集为了显示不同的字体。
TextMeshPro Text
Text Mesh Pro(TMP)替代了Unity现有的文本组件。TextMesh Pro使用Signed Disatance Fild(SDF)作为首选文本渲染管线,使其可以在任意尺寸和分辨率中清晰的渲染文本。使用自定义的shader来提升SDF文本渲染的能力,TextMesh Pro可能通过简单的改变材质来动态地改变视觉效果。
Text mesh rebuilds(Text网格重建) Unity内置的UIText组件在改变文本的时候将触发Canvas.SendWillRendererCanvas方法和Canvas.BuildBatch方法。将TextMeshProUGUI组件中的文本变动最小化并且将其发生变化的组件放置到专门的画布上,使画布重建效率达到最高。 在文本需要显示在世界空间的时候,建议直接使用TextMeshPro,将更加高效,因为他不会产生画布开销。
Fonts and memory usage(字体与内存使用) TMP不支持动态字体功能。
TMP的字体在被场景或项目引用时加载。如果字体资源被TMP Setting资源引用,那么这些字体资源及其全部备用字体资源会在第一个含有TMP组件的场景激活时被递归加载。
如果字体资源被TMP组件引用,并且没有通过TMP Setting加载,那么被引用的字体资源及其全部备用字体资源会在TMP组件被激活时加载。当项目中有很多字体时,需要留意这一过程,尤其是在可用内存不足时。
当程序需要本地化的时候,执行一个引导步骤来检测用户区域并为每个字体资源设置备用字体资源: 1.给基础的TMP字体图集创建AB包 2.给每种语言所需的备用TMP字体资源创建AB包 3.引导过程中加载基础的AB包 4.根据用户区域,加载备用字体的AB包 5.为每个基础字体,从本地化的字体AB包中分配备用字体资源 6.继续游戏
Best Fit and performance TMP不支持动态字体,所以UGUI UIText中的适配产生的问题并不会发生。在TMP上使用适配的时候,唯一要考虑的使二叉树查找合适的大小。在使用自动大小时候最好进行最长最大文本块测试。一旦确定了合适的石村,就该禁用组件的自动尺寸,并手动设置其他文本对象的最佳字号。这样可以提升行,并且可以避免因一组文本组件中的字号不一致而导致的不良视觉/排版体验。
Scroll Views
Unity UI的Scroll View使紧随fill rate问题第二常见的性能问题出现的原因。Scroll Views需要大量的UI元素表示其内容。这有两种基本方式填充滚动视图:
- 一次性将滚动视图全部需要的元素进行加载
- 缓存元素,在需要元素的时候重新定位它们
这两种解决方案都会有一些问题。 第一种方案,初始化UI的时候如果UI元素过多,需要大量的时间进行初始化。也增加了Scroll View重建的时间。如果Scroll View中只有少量元素,这种方法将会很实用。 第二种方案,第二种方法需要大量的代码才能在当前的UI和布局系统下正确的实现。下面进一步讨论两种可能的方法。
尽管有这些问题在Scroll View上添加RectMask2D组件仍然是有用的。这个组件确保了在重建Canvas的时候,位于Scroll View之外的元素不会被添加到绘制的列表。
简单的Scroll View 元素池 最简单的实现Scroll View中的对象池,同时保留ScrollView的原生便利性,最简单的方法使采用混合: 为了在UI中布置元素,使布局系统正确的计算滚动视图内容的大小,并允许滚动条正常工作,需要使用具有LayoutElement组件的GameObject作为UI元素的“占位符”。 然后为ScrollView中可见部分的UI元素实例化一个足够更大的UI元素池,并将占位符设置为这些元素的父节点。当ScrollView滚动的时,重用UI有元素以显示滚动到视图中的内容。 这将减少批处理UI元素的数量,批处理成本仅随着画布内的CanvasRenderer数量增加,而不是随Rect Transforms的数量增加。
简单方法存在的问题 任何被重新设置父节点或者调整在父节点下与兄弟节点的顺序的UI元和这个元素的子元素将会被标记为脏元素,并且强制重建他们的Canvas。 出现这种情况的原因没有区分调整父节点和调整与兄弟节点的顺序的回调。这些事件都调用OnTransformParentChanged回调。Unity UI的Graphic类实现了这一回调,调用了SetAllDirty方法。系统确保了Graphic将重建布局和顶点在下一帧渲染之前。 可以为ScrollView中每个元素的根分配Canvas, 这样就限制了之重建那些改变了元素的Canvas而非这个ScrollView。然而,这个操作将增加drawcalls的数量。更进一步,如果ScrollView中的元素不具有可变尺寸,就没必要重新计算整个ScrollView的布局和节点。但是要避免全部重新计算,要实现一个与位置改变相关联的对象池,而不是与重置父节点或改变同级顺序相关的对象池。
基于位置的滚动框对象池 为了避免上述问题,直接通过改变UI元素的位置。如果尺寸没有变化,则无需重建移动的RectTransforms的内容,从而显著提高了ScrollView的性能。要实现这一功能最好去重写ScrollView的一个子类或者写一个自定义的LayoutGroup。后者通常是一个更加简单的解决方案,可以通过实现LayoutGroup抽象类来实现。 在自定义的Layout Group中可以对底层数据进行分析,来判断有多少数据元素必须显示和如何对ScrollView Content的RectTransform进行适当的缩放。可以通过订阅ScrollRect.onValueChanged事件来判断按需重新设置可见元素的位置。