Unity的场景管理与UGUI优化

本次来看看Unity编辑器创建资源优化,一是合理的场景管理,合理使用预制体来定义场景中重复使用的元素,二是关注Unity UI性能的四类问题,UGUI其实平时使用的比较多,但是误区也多(比如多Canvas独立管理)。而且UI的性能和优化往往是最容易被忽略的一点,看似影响不大,但当一个项目达到了瓶颈的时候,在去优化UI,其中的收益还是非常大的,所以还是需要重视的。

场景与预制体优化

场景结构设计原则

国外的公司常用空节点注释的方式进行场景物体区分(场景/关卡工程师),国内偏向于按照功能分类划分。

  • 合理设计场景一级节点的同时,避免场景节点深度太深,一些代码生成的游戏对象如果不需要随父节点进行 Transform 的,一律放到根节点下(由于针对 C/C++ 转到 Unity 的工程师,想法很美好,但是却存在性能问题,而且逻辑复杂,无法做到动静分离);

  • 尽量使用 Prefab 节点构建场景,而不是直接创建的 GameObject 节点;

  • 避免 DontDestroyOnLoad 节点下有太多生命周期过长或引用资源过多的复杂节点对象。Additve 模式添加的场景要尤为注意;

  • 最好为一些需要经常访问的节点添加 tag,静态节点一定要添加 Static 标记;

注意:复杂场景中,对于设置好Tag的节点,使用FindGameObjectWithTag方法取查找该节点更高效

Tag真的是个非常实用的技巧,平时一定要用起来呀!这么好的功能,不用可惜!

Prefab节点构建场景,和直接创建的GameObject的场景对比如上图所示。

关于场景优化还有很多动态的方法,比如场景烘培、遮挡剔除、场景划分、LOD、HLOD等很多方法。Unity还提供了一些系列场景编辑提效的工具,比如Polybrush,ProBuilder;

预制体优化

预制体Prefab

Unity中的预制体是用来存储游戏对象、子对象及其所需组件的可重用资源,一般来说预制体资源可充当资源模版,在此模版基础上可以在场景中创建新的预制体实例。

使用预制体的好处

1、由于预制体系统可以自动保持所有实例副本同步,因此可以比单纯地简单复制粘贴游戏对象做到更好的对象管理。

2、此外通过预制体嵌套(Nested Prefabs)可以将一个预制体嵌套到另一个预制体中,从而创建多个易于编辑的复杂游戏对象层级视图。

3、可以通过覆盖各个预制体实例的设置来创建预制体变体(Prefabs Variant),从而可以将一系列组合在一起形成有意义预制体的变体。

嵌套预制体与单预制体相比的优点与缺点

优点

  • 嵌套预制体方便预制体管理,方便资源重复利用,易于统计场景复杂度

  • 美术制作时可以比较合理的分配UV,和贴图利用率

  • 方便关卡设计人员发挥,充分合理利用资源

  • 嵌套预制体比较方便利用工具做LOD,LOD效果也比较好

  • 嵌套预制体修改方便,只需修改子预制体就可以做到所有嵌套预制体同步

  • 比较方便做场景遮挡剔除,可以做到精细的遮挡剔除优化效果

缺点

  • 手动做Bundle依赖时要按Scene方式处理,依赖关系较为复杂

  • 可能会增加材质数量与Drawcall数量

  • 不太适合做大规模远景对象,可以用模型替代的方式来做一些模型面片

  • 美术与关卡设计人员要充分考虑组合复杂度与特例场景显示,避免重复性和单一性,需要更多的沟通成本

使用Prefab变体的一些限制

  • 不能改变本体Prefab游戏对象 (GameObject)层级

  • 不能删除本体Prefab中的游戏对象,但可以通过Deactive游戏对象来达到与删除游戏对象同样的效果

  • 对于Prefab变体要保持其Override属性的变化,不能通过Apply to base把这些变化应用到本体Prefab上,这样会破坏基础Prefab的结构和功能。

关于Prefab变体需要知道两点:

Prefab变体与Prefab相当于是子类与父类的关系,Perfab变体其实本质还是Perfab的实例对象;

UGUI优化

Unity UI性能的四类问题

1、Canvas Re-batch 时间过长

2、Canvas Over-dirty,Re-batch次数过多

3、生成UI渲染网格顶点时间过长

4、Fill-rate overutilization,即UI Shader中片元着色器利用率过高

Canvas负责管理UGUI元素,负责UI渲染网格的生成与更新,并向GPU发送DrawCall指令。

Canvas是一个Native层实现的Unity组件,被Unity渲染系统用于在游戏世界空间中渲染分层几何体(layered geometry)。 Canvas负责把它们包含的Mesh合批,生成合适的渲染命令发送给Unity图形系统。以上行为都是在Native C++代码中完成,我们称之为Rebatch或者Batch Build,当一个Canvas中包含的几何体需要Rebacth时,这个Canvas就会被标记为Dirty状态。

Canvas Re-batch过程

1、根据UI元素深度关系进行排序

2、检查UI元素的覆盖关系

3、检查UI元素材质并进行合批

上图就是Canvas Over-dirty,Re-batch次数变多的情况。

UGUI渲染细节

UGUI中渲染是在Transparent半透明渲染队列中完成的,半透明队列的绘制顺序是从后往前画,由于UI元素做Alpha Blend,我们在做UI时很难保障每一个像素不被重画,UI的Overdraw太高,这会造成片元着色器利用率过高,造成GPU负担。

UI SpriteAtlas图集利用率不高的情况下,大量完全透明的像素被采样也会导致像素被重绘,造成片元着色器利用率过高;同时纹理采样器浪费了大量采样在无效的像素上,导致需要采样的图集像素不能尽快的被采样,造成纹理采样器的填充率过低,同样也会带来性能问题。(这个在Unity性能优化的纹理优化篇有讲到)

Re-Build过程

Re-Build是在Re-batch过程中完成的,主要逻辑在C#层,用来重新计算Layout布局与渲染网格重建

在WillRenderCanvases事件调用PerformUpdate::CanvasUpdateRegistry接口

  • 通过ICanvasElement.Rebuild方法重新构建Dirty的Layout组件

  • 通过ClippingRegistry.Cullf方法,任何已注册的裁剪组件Clipping Compnents(Such as Masks)的对象进行裁剪剔除操作

  • 任何Dirty的 Graphics Compnents都会被要求重新生成图形元素

Layout Rebuild

  • UI元素位置、大小、颜色发生变化

  • 优先计算靠近Root节点,并根据层级深度排序

Graphic Rebuild

  • 顶点数据被标记成Dirty,触发Rebuild过程

  • 材质或贴图数据被标记成Dirty,触发Rebuild过程

使用Canvas的基本准则

  • 将所有可能打断合批的层移到最下边的图层,尽量避免UI元素出现重叠区域;

  • 可以拆分使用多个同级或嵌套的Canvas来减少Canvas的Rebatch复杂度,无论同级或嵌套,Canvas都是单独处理的;

  • 拆分动态和静态对象放到不同Canvas下;

  • 不使用Layout组件,就不会发生Layout Rebuild 过程了

  • Canvas的RenderMode尽量Overlay模式,减少Camera调用的开销

UGUI射线(Raycaster)优化

  • 必要的需要交互UI组件才开启 Raycast Target

  • 开启 Raycast Targets的UI组件越少,层级越浅,性能越好

  • 对于复杂的控件,尽量在根节点开启 Raycast Target

  • 对于嵌套的Canvas,OverrideSorting属性会打断射线,可以降低层级遍历的成本

UI字体优化

  • 避免字体框重叠,造成合批打断(上面已经说过)

  • 字体网格重建的情况,应该尽量避免

    • UIText组件发生变化时
    • 父级对象发生变化时
    • UIText组件或其父对象enable/disable时
  • TrueTypeFontImporter支持TTF和OTF字体文件格式导入

动态字体与字体图集

  • 运行时,根据UIText组件内容,动态生成字体图集,只会保存当前Actived状态的 UIText控件中的字符

  • 不同的字体库维护不同的Texture图集

  • 字体Size、大小写、粗体、斜体等各种风格都会保存在不同的字体图集中(有无必要保留各种风格,没必要可以使用第三方工具进行剔除,影响图集利用效率,一些利用不多的特殊字体可以采用图片代替或使用Custom Font,Font Assets Creater创建静态字体资源)

  • 当前Font Texture不包含UIText需要显示的字体时,当前Font Texture需要重建

  • 如果当前 Texture 图集太小,系统也会尝试重建,并加入需要使用的字形,按照2的幂次扩大,文字图集只增不减

  • 利用Font.RequestCharacterInTexture可以有效降低启动时间和运行时字体动态扩展的时间

另外建议将TextMeshPro作为Unity文字处理的最佳方案,https://docs.unity3d.com/cn/2021.1/Manual/com.unity.textmeshpro.html ,TextMeshPro(简称TMP)可以以任意点大小和分辨率清晰的渲染文本,并且在视觉特效上可以使用材质预设来处理,阴影、轮廓、倒角、光韵等效果,而且效率更高。

UI控件优化注意事项

  • 不需要交互的UI元素一定要关闭Raycast Target选项

  • 如果是较大的背景图的UI元素建议也要使用Sprite的九宫格拉伸处理,充分减小UI Sprite大小,提高UI Atlas图集利用率

  • 对于不可见的UI元素,一定不要使用材质的透明度控制显隐,因为那样UI网格依然在绘制,也不要采用 active/deactive UI控件进行显隐,因为那样会带来gc和重建开销,尽量通过Canvas控件的激活与关闭来控制。

  • 使用全屏的UI界面时,要注意隐藏其背后的所有内容,给GPU休息机会,避免设备发热量过大。

  • 在使用非全屏但模态对话框时,建议使用OnDemandRendering接口,对渲染进行降频。注意是使用OnDemandRendering,不能调整TargetPFS,OnDemandRendering 只会降低渲染频率,不会降低输入频率。而直接调整TargetPFS,渲染频率和输入频率都会降低,这显然不是我们所期望的。

  • 优化裁剪UI Shader,根据实际使用需求移除多余特性关键字。

滚动视图Scroll View优化

使用RectMask2d组件裁剪,不要使用不规则的视图,因为RectMask2d可以充分利用模版缓冲;

使用基于位置的对象池作为实例化缓存,这仅仅是提供一个思路而已;

参考资料

主要内容来自Bilibili的Metaverse大衍神君的 2022新年计划《Unity性能优化》 ,其中的大部分笔记摘抄自其PPT作为以后参考。