首页 > 技术文章 > Unity UGUI 规范 优化 大全

hiker-online 2020-09-04 21:22 原文

精要:

Layout:布局组件,控制RectTransform位置大小
Graphic:渲染组件,如image,text
Batch:把符合规则的UI元素集合起来,一次性渲染,规则是材质相同,层级中间没有不同材质,Batch后会缓存,如果UI元素没变化,可快速完成渲染,有变化需要走rebuild
Canvas:画布,画布内的UI有Batch关系,Canvas之间没有
CanvasUpdateRegistry: 检测Graphic与Layout的更新,dirty时
Rebuild: Layout更新(需要排序,从下到上),Graphic更新(不需排序,顶点数据有变化,要重建网格;材质数据有变化,Render更新

Profile测试,常见问题点:
ClipperRegistry.Cull: 更新RectMask2D下面UI的裁剪,一般情况当ScrollRect比较多时,开销较高
Canvas.SendWillRenderCanvases: UI元素变化,引起rebuild,如果下边Layout高,说明位置大小发生了变化;Render高,说明材质发生了变化
Canvas.SendWillRenderCanvases持续高,UI里有动态元素,引起频繁重建
Canvas.BuildBatch或UpdateBatches高,Canvas上的Render太多
UI Render开销高,大概率是OverDraw导致的
WillRenderCanvas中IndexedSet_Sort或SortLayoutList时间高,应该是Layout组件数量多
Text_OnPopulate,可能与开了Best Fit有关
Outline_ModifyMesh,Shadow_ModifyMesh,与字体的描边,阴影有关

优化方法:
全屏UI显示时,禁用3D Camera,或者用一个快照做假
不需要Render,但需要有raycast的UI,去掉Render
动静UI分离,让rebuild范围最小化

 
UI规范总结
    1. 同一个UI界面的图片尽可能放到一个图集中,这样可以尽可能的降低drawcall
    1. 共用的图片放到一个或几共享的图集中,例如通用的弹框和按钮等;相同功能的图片放到一个图集中, 例如装备图标和英雄头像等;这样可以降低切换界面的加载速度
    1. 不同格式的图片分别放到不同的图集中,例如透明(带Alpha)和不透明(不带Alpha)的图片,这样可以减少图片的存储空间和占用内存。(UGUI的sprite packer会自动处理这种情况)???这样会不会经常打断渲染的顺序,增加批次
    1. 单独场景内的UI与其他场景的UI不要混用(比如,战斗、英雄场景)
    1. 图片分辨率能降就降,在不太影响UI表现的前提下
    1. 图片格式要用ASTC,除非有特殊要求的图片
    1. 及时删除UI中的失效节点、脚本、动画,还有一些Disable的废弃内容,这些内容会影响加载时间与内存占用
    1. 减少Rebuild的频率,将动态UI元素(频繁改变例如顶点、alpha、坐标和大小等的元素)与静态UI元素分离出来,放到特定的Canvas中
    1. 使用尽可能少的UI元素;在制作UI时,一定要仔细查检UI层级,删除不不必要的UI元素,这样可以减少深度排序的时间,以及Rebuild的时间
    1. 谨慎使用UI元素的enable与disable,因为它们会触发耗时较高的rebuild,替代方案之一是enable和disableUI元素的canvasrender或者Canvas
    1. Text的Best Fit选项尽量不要用,虽然这个选项可以动态的调整字体大小以适应UI布局而不会超框,但其代价是很高的,Unity会为用到的该元素所用到的所有字号生成图元保存在atlas里,不但增加额外的生成时间,还会使得字体对应的atlas变大。
    1. 描边,字体阴影尽量不要用,用的话推荐用TextMeshPro的
    1. 少用Canvas的Pixel Perfect选项,该选项会使得ui元素在发生位置变化时,造成layout Rebuild。(比如ScrollRect滚动时,如果开启了Canvas的pixel Perfect,会使得Canvas.SendWillRenderCanvas消耗较高)
    1. 使用缓存池来保存ScrollView中的Item,对于移出或移进View外的的元素,不要调用disable或enable,而是把它们放到缓存池里或从缓存池中取出复用
    1. 不要使用空的Image,在Unity中,RayCast使用Graphi作为基本元素来检测touch,在笔者参与的项目中,很多同学使用空的image并将alpha设置为0来接收touch事件,这样会产生不必要的overdraw。如果需要用,请用Block
    1. 少用引起频繁Rebuild的功能组件:Layout
Canvas.BuildBatch原理
Canvas.BuildBatch主要功能是合并Canvas节点下所有UI元素的网格,合并后的网格会缓存起来,只有其下面的UI元素的网格发生改变时才会重新合并。而UI元素的网络变化主要是因为Canvas.SendWillRenderCanvases调用时,rebuild了Layout或者craphic
  1. 该过程由CanvasUpdateRegistry监听Canvas的WillRenderCanvases(上图中1)而执行,主要是对前标记为dirty的layout和craphic执行rebuild。引起layout和graphic的dirty主要原因是因为Canvas树形结构下的UI元素发生了变化(例如增加删除UI对象,UI元素的顶点,rec尺寸改变等)调用了Graphic.SetDirty(实际上最终都会调用CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild)。
  1. 在rebuild layout之前会对Layout rebuild queue中的元素依据它们在heiarchy中的层次深度进行排序(上图中的2),排列的结果是越靠近根的节点越会被优先处理。
  1. rebuild layout(上图中的3),主要是执行ILayoutElement和ILayoutController接口中的方法来计算位置,Rect的大小等布局信息。
  1. rebulid graphic(上图中的4),主要是调用UpdateGeometry重建网格的顶点数据(上图中5)以及调用UpdateMeterial更新CanvasRender的材质信息(上图中6)。
Depth计算算法
  • a) 遍历所有UI元素(已深度优先排序),对当前每一个UI元素CurrentUI,如果不渲染,CurrentUI.depth = -1,如果渲染该UI且底下没有其他UI元素与其相交(rect Intersects),其CurrentUI.depth = 0;
  • b) 如果底下有一个的需要渲染的UI元素LowerUI与CurrentUI相交的情况下,且
  • 可以Batch(material instance id 和 texture instance id 相同),depth_i = LowerUI.depth;
  • 不可以Batch,depth _i= LowerUI.depth + 1;
  • 如果底下有n个UI元素与CurrentUI相交,根据b)计算n个的depth_i取最大的作为CurrentUI的depth:  CurrentUI.depth = Max( depth _1, depth _2, depth _3 ... , depth _n)。
  • Depth计算完后,根据Depth排序UI Instructions,如果Depth相等,依次根据材质ID、texture ID、渲染顺序(即UI层级队列顺序)排序,剔除depth == -1的UI元素,得到Batch前的UI 元素队列VisiableList。
 
  • 对VisiableList中相邻且可以Batch(相同material和texture等)的UI元素合并批次,然后再生成相应mesh数据进行绘制。
 
 
  • 注意:在Depth计算算法中,由于要遍历所有UI元素和已计算的底层UI元素(平方复杂度),源码中使用分组计算包围盒矩形的方法加快计算,即16个UI元素为一组计算Group Rect,检查是否与底层UI元素相交时,先计算是否与底层Group相交,如果相交再与Group中的元素做判定。
  • 因此,UI元素数目过多和层次结构过于复杂,会影响排序和Batch更新速度,合理规划UI元素数量和层次结构可以提高UI性能。
 
UI Rebuild
Graphic Rebuild:引起原因(位置、颜色)
Layout Rebuild: Enable/Disable、位置、大小,根结点发生变化会影响子节点;UI层次结构发生变化,新增、删除UI或UI子节点
函数体现:Canvas.SendWillRenderCanvases或Canvas.BuildBatch
 
排序开销
在渲染前,需要对一个Canvas下的元素排序,以决定哪些元素可以一个批次渲染(涉及OverDraw),如果Canvas下的层级关系复杂,那么排序的开销会呈现非线性增长!!重新合批用到了多线程,所以与手机硬件也有关系。
FillRate
目前在移动设备上,FillRate 的压力主要来自半透明物体。因为多数情况下,半透明物体需要开启 Alpha Blend 且关闭 ZTest和 ZWrite,同时如果我们绘制像 alpha=0 这种实际上不会产生效果的颜色上去,也同样有 Blend 操作,这是一种极大的浪费
Mask
注意Mask的使用,避免没必要的Mask增加DC
首先一个Mask组件就会产生一个Draw Call,而且在Mask中的图片无法与外界的图片进行合批
事件检测的优化     
UGUI在手指点击的时候,其实是Canvas上挂的一个Graphic Raycaster组件去遍历整个Canvas下所有UI元素下RayCast Target属性是否允许点击,所以我们可以把不允许点击的UI元素反勾选掉这个选项,减小开销
动静分离
具体做法是把正在移动的元素挂上一个Canvas,并修改它的sort order,为了操作方便我们是在脚本中去检测到当前移动的元素,并给它挂上Canvas。当不再移动后,再销毁这个Canvas即可。
改颜色优化
对于一些需要高亮时,边框颜色渐变的需求,我们如果直接去修改Image上自带的Color属性实际上是修改的是它的顶点上的属性,触发Graphic Rebuild,这样的话会造成整个Canvas的网格重建,所以我们可以去自己创建一个自定义材质赋给Image,去改变材质的Tint Color属性。
OverDraw优化
在进行UI设计时,尽量少使用来自不同Atlas的材质,也尽量避免这种重叠的情况(Scene视图调节为Writeframe模式可以更容易查看),尽可能把所有的文字放到图片之上,使用同种字体,更容易进行合批
补充一个解决方案:针对一些重UI的2D游戏,可以将多层的静态UI渲染到一张纹理上,减少后续的OverDraw
 
原理介绍比较详细:
操作,贴图比较详细:
 

推荐阅读