Unity UGUI优化

Unity UGUI 优化

原内容来自于 雨松MOMO的UWA课堂

纹理格式的选择

界面打开慢可分为首次打开慢和再次打开慢,首次打开慢一般是由于需要加载过多的UI资源。而再次打开慢就是程序不合理造成的了。 首次界面打开加载的资源(如:贴图)会被缓存在内存中,这样再次打开界面由于内存中已经有了资源(如:贴图)所以会更快。 作为界面优化,我们应当尽可能地让首次打开得更快。

Android平台

不带透明通道优先使用ETC1,而带透明通道的优先使用ETC2.
如果显示质量无法达到要求还可以使用RGBA16,最后才使用RGBA32.
总体来说正确使用优先顺序是ETC1>ETC2->RGBA16->RGBA32.
另外针对Android平台Unity还实现了一套Crunched压缩方式,比如RGBA Crunched ETC2压缩格式,会先用ETC2进行压缩,然后再用Crunched压缩一遍。虽然运行时逻辑上需要再额外解压缩一遍,但是由于Crunched压缩会让贴图大小更小,加载的时间会比单纯加载ETC2快很多。总体来说RGBA Crunched ETC2会比ETC2加载更快,而且包体会更小.

纹理优化:通道分离

针对Android平台Unity还提供了一种通道分离的方式: 将图片压缩成ETC1,提取Alpha生成一张通道图. 为了让混合起来的Alpha效果更好,Unity将通道图保存的格式设定为a8格式。 比如一张1024X1024的贴图,ETC1压缩结果为0.5M,通道图提取后a8格式压缩结果为1M,加起来就是1.5M。对比直接使用ETC2压缩1024贴图为2M,前者节省了0.5M内存。 注意 虽然使用通道图内存上可以减少一些,但是在Shader中需要进行2次采样,综合看在某些机器上未必性能会得到提升。

iOS平台

如果没有透明通道那么使用PVRTC来压缩必然是首选 优先使用PVRTC,其次使用ASTC. 不带透明通道可以使用ASTC 5X5(表示每个压缩块的大小是5 X 5=25),带透明通道可以使用 ASTC 4X4(表示每个压缩块的大小是4 X 4=16).
如果显示质量无法达到要求还可以使用RGBA16,最后才使用RGBA32,总体来说正确的使用优先级顺序是PVRTC>ASTC->RGBA16->RGBA32

总结

以一张1024X1024的贴图为例:

  • RGBA32 Bit:表示每个像素占用32位4字节,内存大小 1024 X 1024 X 4 = 4M
  • RGBA16 Bit:表示每个像素占用16位2字节,内存大小 1024 X 1024 X 2= 2M
  • RGB ETC1 4Bit: 表示每个像素占用4位0.5字节,内存大小 1024 X 1024 X 0.5= 0.5M
  • RGBA ETC2 8Bit: 表示每个像素占用8位1字节,内存大小 1024 X 1024 X 1= 1M
  • RGBA PVRTC 4Bit: 表示每个像素占用4位0.5字节,内存大小 1024 X 1024 X 0.5= 0.5M
  • RGBA ASTC 4X4 : 表示每个像素占用8位1字节,内存大小 1024 X 1024 X 1= 1M

很显然占用内存越小的贴图,加载速度肯定就越快,那么打开这样的界面无疑也就越快了。

纹理尺寸注意事项

并不是所有图片都需要打图集的,因为一旦图片打进图集,哪怕仅仅只需要显示这个图集上的一小部分,也会把整个图集拉进内存中。 所以我们会将宽高超过128或者256的图从图集中拿出来,比如游戏中的一些玩家头像,背景图等等。

IOS上如果没有透明通道那么使用PVRTC来压缩必然是首选. 如果一些玩家的头像设计上就不是正方形,如果恰巧头像没有半透,那么使用ASTC岂不是浪费了。

由于图集肯定满足2的幂次方,所以我们将这张图变成一个单独的图集就可以进行正确的压缩了 而且图片不会模糊,如果使用Non Power of 2拉伸图片就会模糊。

注意

  • ETC1ETC2以及ASTC 4X4 要求图片宽和高可以不相等但是必须被4整除,例如512X1024 4X16
  • IOS的PVRTC压缩格式要求图片的宽高必须相等并且是2的整数次幂,例如512X512,如果是512X1024那么就无法压缩了。
  • 另外ETC2和ASTC 对硬件也有一些限制,ETC2只支持OpenGL ES 3.0以上的Android手机(大概2013年以后的手机都支持)ASTC只支持苹果A8以后的设备,iPhone 6 及以上的手机(大概2014年以后的手机都支持)。

UI点击事件性能优化

UGUI的事件本质上就是发送射线,由于UI的操作有一些复杂的手势,所以UGUI帮我们又封装了一层。 创建任意UI时都会自动创建EventSystem对象,并且绑定EventSystem.cs和StandaloneInputModule.cs如下代码所示,EventSystem会将该对象绑定的所有InputModule脚本收集起来保存在SystemInputModules对象中。

原因

当发生点击时,RaycasterManager.GetRaycasters();方法就是获取当前到底有多少个绑定GraphicRaycaster脚本的对象,那么同时参与点击事件的Canvas越多效率也就越低了.

由于多个UI有相交的情况,但由于Mesh都合批了第一个与射线相交的对象是没有意义的,但是我们只需要响应在最上面的UI元素,这里只能根据depth来做个排序了,找到最上面的UI元素,最后再抛出正确的点击事件。

游戏中有很多界面是叠在一起的,最上面的界面已经挡住了所有界面,但是由于下面的界面还有GraphicRaycaster对象,那么必然产生额外的计算开销.

所以说GraphicRaycaster组件越多越卡,raycastTarget勾选的越多越卡.

解决方案

  • 开发中很多UI是不需要响应点击事件的,但是却被无意地勾选上了raycastTarget。把不需要响应点击事件的UI 取消勾选raycastTarget.
  • 游戏中有很多界面是叠在一起的,最上面的界面已经挡住了所有界面,但是由于下面的界面还有GraphicRaycaster对象,那么必然产生额外的计算开销,所以这种情况可以DeActive不需要参与点击事件的Canvas。

优化工具

下面的代码可以在Scene视窗中标记出勾选raycastTarget响应点击事件的UI

#if UNITY_EDITOR using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class DebugUILine : MonoBehaviour {
	static Vector3[] fourCorners = new Vector3[4];
	void OnDrawGizmos()
	{
		foreach (MaskableGraphic g in GameObject.FindObjectsOfType<MaskableGraphic>())
		{
			if (g.raycastTarget)
			{
				RectTransform rectTransform = g.transform as RectTransform;
				rectTransform.GetWorldCorners(fourCorners);
				Gizmos.color = Color.blue;
				for (int i = 0; i < 4; i++)
					Gizmos.DrawLine(fourCorners[i], fourCorners[(i + 1) % 4]);
 
			}
		}
	}
}
#endif

Mask遮罩组件 和 RectMask2D遮罩组件的选择.

UGUI的裁切分为Mask和RectMask2D两种,我们先来看Mask。 它可以给Mask指定一张裁切图裁切子元素。 我们给Mask指定了一张圆形图片,那么子节点下的元素都会被裁切在这个圆形区域中。 功能确实很强大,我们来看看它的效率如何呢?

Mask

介绍

由于裁切需要同时裁切图片和文本,所以Image和Text都会派生自MaskableGraphic。 如果要让Mask节点下的元素裁切,那么它需要占一个DrawCall,因为这些元素需要一个新的 Shader参数来渲染。

通过查看源码可知 Image对象在进行Rebuild()时,UpdateMaterial()方法中会获取需要渲染的材质,并且判断当前对象的组件是否有继承IMaterialModifier接口,如果有那么它就是绑定了Mask脚本,接着调用上面提到的GetModifiedMaterial方法修改材质上Shader的参数。

Mask的原理就是利用了StencilBuffer(模板缓冲),它里面记录了一个ID,被裁切元素也有StencilBuffer(模板缓冲)的ID,并且和Mask里的比较,相同才会被渲染。因为模板缓冲可以提供模板的区域,也就是前面设置的圆形图片,所以最终会将元素裁切到这个圆心图片中。 如图所示,在Mask外面放一个普通的图片,默认情况下Stencil Ref的值是0,所以它不会被裁切,永远会显示出来。

性质

Mask组件需要依赖一个Image组件,裁剪区域就是Image的大小。

Mask会在首尾(首=Mask节点,尾=Mask节点下的孩子遍历完后)多出两个drawcall,多个Mask间如果符合合批条件这两个drawcall可以对应合批(mask1 的首 和 mask2 的首合;mask1 的尾 和 mask2 的尾合。首尾不能合)

计算depth的时候,当遍历到一个Mask的首,把它当做一个不可合批的UI节点看待,但注意可以作为其孩子UI节点的bottomUI。

Mask内的UI节点和非Mask外的UI节点不能合批,但多个Mask内的UI节点间如果符合合批条件,可以合批。

RectMask2D

介绍

通过查看源码可知 Mask2D会在OnEnable()方法中,将当前组件注册ClipperRegistry.Register(this);这样在上面ClipperRegistry.instance.Cull();方法时就可以遍历所有Mask2D组件并且调用它们的PerformClipping()方法了。

PerformClipping()方法,需要找到所有需要裁切的UI元素,因为Image和Text都继承了IClippable接口,最终将调用Cull()进行裁切。

RectMask2D会将RectTransform的区域作为_ClipRect传入Shader中,并且激活UNITY_UI_CLIP_RECT的Keywords。Stencil Ref 的值是0 表示它并没有使用模板缓冲比较,如果只是矩形裁切,RectMask2D并且它不需要一个无效的渲染用于模板比较,所以RectMask2D在特定情况下的效率会比Mask要高。

性质

RectMask2D不需要依赖一个Image组件,其裁剪区域就是它的RectTransform的rect大小。

RectMask2D节点下的所有孩子都不能与外界UI节点合批且多个RectMask2D之间不能合批。

计算depth的时候,所有的RectMask2D都按一般UI节点看待,只是它没有CanvasRenderer组件,不能看做任何UI控件的bottomUI.

总结

  • 当一个界面只有一个mask,那么,RectMask2D 优于 Mask
  • 当有两个mask,那么,两者差不多。
  • 当大于两个mask,那么,Mask 优于 RectMask2D。

Layout布局排序组件的性能问题

UGUI的布局功能确实很强大,只要挂在节点下就可以设置HorizontalLayoutGroup(横向)、VerticalLayoutGroup(纵向)、GridLayoutGroup(表格)的布局了。

问题

虽然使用方便,但是效率是不高的,这里我们以纵向来举例。无论横向还是纵向排列,首先得计算出每个子对象的区域才行。 通过查看源码可知: 最核心的计算在LayoutUtility. GetLayoutProperty()方法中,把每个实现ILayoutElement接口的对象的信息取出来。 由于Image和Text都实现了ILayoutElement接口,所以LayoutGroup下的Image和Text元素会自动布局,也可以绑定LayoutElement脚本主动设置区域。 但是Layout还有Min Wdith和Flexible Width可设置最小宽高和弹性宽高,这都需要进行额外的计算产生额外的开销,如果对效率要求比较高的UI,最好可以考虑自行封装一套布局组件。而且当他排序布局时,它们势必会导致所有元素的Rebuild()执行两次。 1、界面第一次打开需要进行第一次Rebuild() 2、Layout组件要算位置或者大小会强制再执行一次Rebuild()

很有可能有些元素是不需要Rebuild的,但是Layout组件也会强制执行,那么势必造成额外的开销。


优化技巧

界面的操作慢会更复杂一些,在操作界面之前要先确定当前的渲染是否已经存在瓶颈。操作界面一般会触发UI的开关或者隐藏显示,这必然造成UI重建,可以观察具体瓶颈。

  1. 操作界面之前以及操作界面之后确定UI是否在无意义重建,可以参考文章前面提到的方法得到到底哪些UI元素了引起UI的整个重建。
  2. 避免Active和DeActive,而采用修改显示layer的方式来控制隐藏显示。
  3. 不需要参与点击事件的Canvas取消激活Graphic Raycaster脚本。
  4. 仅用于显示的图片或者文本禁止勾选Raycast Target。
  5. 持续性的UI动态效果特效,最好脱离UI系统,采用特效的方式制作。
  6. 动态和静态的UI要区别对待,分别挂上canvas。
  7. 适当对UI界面做缓存,保证再次打开更快。
  8. 界面初始化代码部分添加上Profiler监测,统计代码效率。
  9. 将复杂的界面拆成多个界面,比如Tab页点击切换时再加载。
  10. 再次打开界面也需要控制一帧内加载UI的数量,做好分帧加载策略。

普通界面就像一般的背包装备界面,如果可以设计成全屏界面,那么还可以关掉3D摄像机。没有了3D部分的渲染,那么效率必然会有所提升。而且普通界面的UI一般就是按钮和滑动列表两种,此时的帧率即使掉到了25帧玩家也是可以接受的。但是战斗界面就不一样的,因为战斗期间帧率一旦掉到了30帧,玩家都能感受到,所以说战斗界面操作慢是完全不能忍受的。

战斗界面是战斗时玩家可操作的界面,也是游戏中最敏感的界面,只要有一点卡顿玩家就能很明显感知到,假设游戏战斗保持45FPS,那么留给每一帧的时间只有22ms,留给UI的只会更少。

  1. 通过Profiler查看UI的渲染耗时,到底是卡在GPU提交还是渲染上。
  2. 左边接收任务框或者右上方活动上的UI转圈动画,最好用粒子特效来做。
  3. 头顶文字称号和冒血数字最好不要使用UI系统,可用SpriteRenderer和3D Text实现。
  4. 头顶文字称号和冒血数字使用UI系统的缺陷:摄像机位置角度发生变化需要实时修改UI位置,必然会造成网格合并,还有UI系统本身效率就没有3D的高。
  5. 下方图文混排聊天,一帧只加载一个,避免消息太多造成卡顿。
  6. 技能CD转圈不要每一帧都更新,并且套上新的canvas。
  7. 右上角鹰眼小地图最好不要使用Mask裁切,采用RawImage修改UVRect的方式进行裁切,降低填充率。
  8. 界面中显示倒计时,由于最小单位是秒,最好不要每一帧都计算。
  9. 战斗界面上(左、右)、下(左、右)、左、右、中。共9个部分都要挂上不同的Canvas避免网格重建影响太多。
  10. 主界面首次打开可能会比较慢,会影响Loading条的等待时间,可以考虑分帧加载UI。

非战斗界面比战斗界面的效率要求要低一些,因为界面几乎已经挡住战斗画面,单纯操作界面,保持在30FPS就可以了。

  1. 非战斗界面最好设计成全屏UI,这样打开时就可以将战斗的Camera隐藏,提高效率。
  2. 重点关注游戏对象Active和DeActive的数量。
  3. 界面上显示3D模型做好分帧加载。
  4. 滑动列表的滑动效率需要重点关注。
  5. 如果Image有n种不同的样式,最好不要创建n个Image对象代码控制隐藏显示,可以考虑做一个通用UI状态组件来切换。
  6. 文本组件建议使用Text Mesh Pro。

  •  
发布了42 篇原创文章 · 获赞 23 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/gaojinjingg/article/details/103564828