0%

UGUI自适应与优化与其他优化

UGUI自适配与优化

UGUI自适配与优化

 

 

RectTransform

继承自Transform,是UGUI中最重要的组件之一

  • Anchored(锚点)
  • Pivot(中心点)
  • Rotation(旋转)
  • Size Delta(大小)

piFo1sO.png

 

锚点

RectTransform rectTransform = GetComponent<RectTransform>();
rectTransform.anchoredPosition = new Vector2(100, 100);

锚点作用于父类子节点,也就是说如果UI对象有一个子对象,那么这个子对象就会基于父节点的锚点来进行位置偏移(这个父对象往往是添加UI后默认增加的“canvas”)。如果某节点没有父节点,那么锚点预设无法设置。锚点的意思是子类的中心点(Pivot)相对父类锚点(Anchored)位置不变,这个位置可以设置XYZ,可以理解成相对偏移。

面板上的田子图一样的东西,就是锚点预设,表示设置子类锚在父类上的点。你可以通过“锚点预设”来直接点击设置锚点位置与锚点大小,也可以直接在检查器内输入数字来进行设置。

锚点属性是一个二维数组,这个有什么含义呢,如果我们设置锚点为一个点并将其锚在父类的左下角

piF7vZD.png

那么子类(红色)的中心位置就会严格跟随这个锚点

piFHPzt.png

如图是将父类(白色)下界往上拖动

再试试更改检查器中的XY,例如把他们都设置为0,0,可以看到红色中心就定在锚点上,意思就是与锚点的”偏移“为0

piFHVeS.png

锚点在检查器中只限制在父类范围以内(似乎通过代码可以突破这个限制?),所以二维数组的大小在0~1以内,意思就是将父类的宽高按比例进行归一了,这里就不演示将锚点位置改变到其他位置了。

二维数组中的”min“表示锚点左下角的位置,”max“表示锚点右上角的位置,也就是说其实锚点其实可以不是一个点。

piFHKWn.png

图中左下角三角形表示最小,右上角表示最大,示例显示了右上角设置为0.5,0.5

piFHJwF.png

再次拖动白色右边界,发现红色的大小也随之变化,因为右上角的锚点与红色右上角(如图中右上角蓝色点)相对位置不会变化,左下角的点与锚点左下角不会相对位移。由于锚点是锚在父类对象上,大小和父类对象成比例点相对位置和子类不变,这是锚点的基本逻辑。

下面几个是更加夸张的例子:

piFHLfs.png

piFHXpn.png

前者直接将白色负向拉伸了,此时锚点相对位置还是不变,后者将锚点直接定为覆盖父类白色全部,此时子类大小完全和父类变化成比例一致。

 

中心点/轴心

RectTransform rectTransform = GetComponent<RectTransform>();
rectTransform.pivot = new Vector2(0.5f, 0.5f);

前面也提到过,可以经过代码设置,同样UI元素的缩放与旋转也以这个为基准

 

旋转

RectTransform rectTransform = GetComponent<RectTransform>();
rectTransform.rotation = Quaternion.Euler(new Vector3(0, 0, 45));

 

缩放

RectTransform rectTransform = GetComponent<RectTransform>();
rectTransform.sizeDelta = new Vector2(200, 100);

 

自适应分辨率

所以为什么UI要设置成这样呢?为什么Unity不直接将其设置成一个真正的画板,然后直接将UI组件在上面直接画?

如果你经常游玩《英雄联盟》,你会注意到这样一个情况:当选择游戏全屏或无边框的时候,游戏下方的技能UI界面大小是完全不一样的。这就是自适应分辨率的一个小表现,试想一下如果使用超大屏幕玩游戏,结果UI界面按比例增大比人脑袋还大,给人的体验并不好。

你试过在电脑上下载安卓模拟器玩游戏的体验吗?

这个在手机上最为明显,因为不同手机由于型号不同屏幕的长宽比例也不同。我不确定你玩没玩过《原神》,但这个例子是用来解释自适应分辨率的最佳对象,因为他是难得的一款双端但是UI差异十分明显的游戏。如果你在电脑上开发原神,结果在安卓手机上游玩的时候圆形的技能图标被压缩成椭圆,或者技能图标因为手机的比例问题直接印在边界下方”被劈成两半“,这些都不是我们想看到的。

你当然可以直接通过代码将所有UI元素按需一个个安排好:

RectTransform rectTransform = GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.anchoredPosition = Vector2.zero;
rectTransform.sizeDelta = new Vector2(Screen.width * 0.8f, Screen.height * 0.8f);

又或者可以通过上面所说的锚点机制来分别设置各UI元素相对父类结点的相对位置,然后父类结点的对象会根据屏幕不同而变化,子节点不变,这个时候自适应分辨率就有了初步的雏形。

 

画布(canvas)

画布 (Canvas) 组件表示进行 UI 布局和渲染的抽象空间。所有 UI 元素都必须是附加了画布组件的游戏对象的子对象。从菜单 (GameObject > Create UI) 创建 UI 元素对象时,如果场景中没有画布 (Canvas) 对象,则会自动创建该对象。

你当然可以将画布改名,但画布组件必须被包含。

画布可以更改不同的渲染方式:

  • Screen Space - Overlay——无视摄像机,所有元素都渲染在其他物体前面,并且UI会根据屏幕自适应
  • Screen Space - Camera——UI在屏幕上的大小不会随距离而变化,这个模式下UI会随着与摄像机距离不断进行缩放来适应,缺陷是UI如果距离太远可以被物体遮挡(也有可能不是缺陷呢是吧)
  • World Space——把UI当作物体渲染在世界中,可以制造全息效果

 

HDR和MSAA的关闭

相机默认启用HDR和MSAA,是屏幕后处理的一种,调用出帧调试器会发现空项目也会执行后处理,所以在不使用HDR和MSAA的时候手动将其关闭就不会进行调用。(直接在摄像机面板上关闭)

 

Stats统计面板

  • Tris——三角形面数
  • Verts——三角形点数

新建新场景,直接打开Stats面板,这个时候这两个数据就是天空盒的数据

在摄像机中取消天空盒,可以得到屏幕的数据

只会统计摄像机视野内的数据

阴影会影响这两个数值

  • SetPass calls——材质调用次数
  • Batches——批处理次数
  • Saved by batching——批处理节省Draw call的次数

这个指标将是进行UGUI性能优化的重要指标之一

 

Draw Call与批处理

试想情况,将十块砖分开搬,还是将十块砖打包一次性搬,哪种会快点?

Draw Call是GPU将模型呈现到屏幕上的过程的次数,其实就是“搬砖”的次数,从结果论上讲Draw Call是越少越好的。

来自知乎@acnestis

渲染流水线的第一步是【CPU和GPU之间的通信】,有如下3个步骤: \1. 把数据加载到显存中:把网格和纹理等数据从硬盘加载到显存中(因为显卡对显存的访问速度更快) \2. 设置渲染状态:CPU根据材质球设置渲染状态,比如,使用哪个顶点着色器/片元着色器、光源属性、纹理等 \3. 调用Draw Call:准备好上述工作后,CPU就调用一个渲染命令(Draw Call)来告诉GPU可以开始渲染啦。

GPU的渲染能力是很强的,渲染200个还是2000个三角网格通常没有什么区别,渲染速度往往快于CPU提交命令的速度。如果一帧中Draw Call数量太多,CPU就会在“设置渲染状态-提交Draw Call”上花费大量时间,造成性能问题。

Draw Call几乎等价于Batches,尽管他们指定的操作并不是一个,但数值上往往是一致的。

批处理就是将“砖块”打包的过程,也就是将美术素材、模型等小网格组成大网格。于是Draw Call次数减少,可以大力提高程序效率。

静态批处理与动态批处理

在projectsetting->玩家中

piFvNBq.png

 

  • 静态批处理

首先在检查器的最右端将对象设置为静态。

piFzj1g.png

标记为静态物体就好。

游戏中不移动不缩放的物体就可以视为静态物体。

静态批处理的对象就是这样的使用相同材质引用静态物体

静态批处理需要额外的CPU内存来存储组合的几何图形,所以其实是一种牺牲内存来加快速度的方式。在个别场景中需要牺牲渲染性能来避免内存占用,例如大森林场景中的树木。

以上,是构建时的静态批处理,unity支持运行时的静态批处理,可调用StaticBatchingUtility类。

 

  • 动态批处理

动态批处理是Unity自动使用的,不需要我们提前设置

 

  • SRP(可编程渲染管线)批处理器

是SRP中特有的

 

  • GPU Instancing

后续会讲到,这里已经有点偏离UGUI了

 

UGUI的合批

就是对UGUI的控件进行批处理

 

帧调试器

窗口->分析->frame debugger

打开帧调试器,启用,会将摄像机渲染的过程全部分开一一呈现在面前。

pikkHWq.png

直接看UGUI那一栏,是UGUI的绘制过程

Draw Mesh——绘制网络,所有的UGUI的控件在unity面前都是网络,这里点击两个Draw Mesh,发现两个Shader与贴图都不一样,因为两个操作对应的分别是图片与文字text组件。

从这里就可以看出来,两者并没有合批。帧调试器会显示摄像机一步一步的绘画过程,所以说我们可以从这个组件间接得出是否合批。

 

Profiler-UI

和帧调试器的路径是一样的

似乎unity5以上的版本调试不出?这里先标记吧

//更新——后来在实际测试情况中调出来了//

总之无法合批的原因就是贴图不一样

 

实验与结果

首先在刚刚建立的项目中去除光照、取消天空盒、取消HDR与MSAA。

还是那两个UI块,结果如下:

pikVSR1.png

Batches值为2

pikViqO.png

帧调试器中只出现一个Draw Mesh

种种现象表明,两个色块合批了。

 

于是加入text组件,位置如下:

pikVZid.png

发现Batches值为3,很显然text组件和普通图片的贴图并不一样,所以没有合批

 

移动一下Text位置:

pikVNzq.png

发现Batches值变为4

pikVyFJ.png

Draw Mesh也变为三个

 

众所周知Unity的渲染顺序是由对象层级来决定的,在上面的会优先渲染。而Text的摆放位置似乎会影响到两个图片的合批,就好像“打断”了他们的合批一样。

这个就涉及到UGUI的合批规则了。

 

UGUI合批规则

  • 两个UI控件能合批的基本条件是这两个控件使用的材质球(Shader)和贴图要完全相同。
  • 合批是以Canvas为单位,不同的Canvas是另一个批
  • 计算深度

参考CSDN@WangShade的计算方法

按照Hierarchy中从上往下的顺序依次遍历Canvas下所有UI元素 对于当前的UI元素CurrentUI i.如果CurrentUI不渲染,则Depth = -1 ii.如果CurrentUI要渲染,但CurrentUI下面没有其他UI元素与其相交,则Depth = 0 iii.如果CurrentUI要渲染,下面只有一个UI元素(LowerUI)与其相交,且CurrentUI与LowerUI可以合批(材质和贴图完全相同),则CurrentUI.Depth = LowerUI.Depth;如果两者不能合批,CurrentUI.Depth= LowerUI.Depth + 1 iv.如果CurrentUI要渲染,下面有n个元素与其相交,则按照步骤iii,分别计算出n个Depth(Depth_1、Depth_2、Depth_3…),然后CurrentUI.Depth取其最大值,即CurrentUI.Depth = max(Depth_1, Depth_2, Depth_3,…) 上面步骤中的“下面”和“相交”要明确下意思,这两个概念很重要。 CurrentUI下面的UI,指Hierarchy面板中,在CurrentUI之上的元素。 ———————————————— 版权声明:本文为CSDN博主「WangShade」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/sinat_25415095/article/details/112388638

这里我理解的并不是很深,详细可以看原文解释。

注意“下面”指渲染层次,也就是说在监视器中“下面”指组件“上面”的组件

总而言之,合批根据Depth来列出一个List,再根据这个List来计算合批,这个时候并不是完全依靠监视器中的List顺序来进行解释。就像上面展示的,由于Text组件与其他两个组件相交,Text“下面”是其中一个图片,所以Text深度为1,这样一来Text“上面”的图片深度就为2了,所以三个组件都不为一个批次。

(可以自己试一下将三个组件覆盖进行改变,可以得出不一样的结果)

 

优化实操

总结如下:

  • 使用图集,将多个小图打包到一个大图中来减少内存占用,合并到一个纹理。
  • 动静分离——减少更新次数
  • 优化UI结构,防止合批中断

 

现在进入最近比赛的项目中尝试进行优化。

piAIQVe.png

游戏还未启动,发现Batches次数已经达到惊人的22层,哪怕只是一个体量非常小的游戏。在运行时跳到30左右。

piAIgMV.png

四个元素给干出9个批出来,恐怕是动态血量造的孽。//后面发现随时间会越来越多。。但是我并不确定这个与时间有没有关系

piAITR1.png

帧调试器界面,发现瓦片地图渲染了十多次。

 

开始优化

首先将金币与生命值划为一个图集

piAT5g1.png

发现PlayerUI中批次直接减少了一次

piAThC9.png

关于血量的问题出在血量条上,这个原素材是用Package里面的,所以设置图集直接闪退炸了,另一个UI由于图片太多出于实验目的暂时不优化,与此同时可以看见帧调试器中的Draw Mesh也少了一次。

piA77zn.png

地图划为一个图集后也显著降低了很多批次。

这里也可以直接将地图划分为静态来启用静态批处理

piAHMQI.png

图中的15只是因为刚好地图中出现了怪物而对照组中未出现,所以理论上比15还要小很多。

做的时候发现地图不能完全合到一个批次,怀疑是地图编辑问题导致合批中断。

初步优化先到这里,虽然说小游戏还看不出优化效果,但是从面板上看还是很有成就感的。

piAHNWj.png

 

 

提高:GPU Instancing

Unity5.4版本之后的新黑科技(虽然说对于现在已经不算什么新货了)

对于静态批处理,需用占用系统大量内存,而对于动态批处理,有时候合批需求过多的情况下动态批处理的做法反而得不偿失,而GPU Instancing可以通过对单个实例进行渲染多个网格对象从而进行大批量相同材质和网格的对象渲染。

可以理解成需要进行大批量相同物件的渲染时,进行GPU Instancing可以避免静态批处理造成的内存损耗,相对来说便减少了渲染时间。

当然物件也可以显示不同属性,例如大型森林可以对树木进行缩放或者颜色改变来控制每个对象的独有性。

 

使用

新建材质/在已有材质上->在监视器中选择“启用GPU实例化”->挂载该材质到预制体上

piAzPlF.png

此时在Unity3D中大量实例化该预制体

piAziy4.png

这里处理得有点糙,天空盒等一系列没有预处理,所以才是4

 

GPU Instancing再深入就是shader相关了,或者一些属性设置,这些留着以后做demo再说吧(如果有机会)