原内容来自于 雨松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的贴图为例:
很显然占用内存越小的贴图,加载速度肯定就越快,那么打开这样的界面无疑也就越快了。
纹理尺寸注意事项
并不是所有图片都需要打图集的,因为一旦图片打进图集,哪怕仅仅只需要显示这个图集上的一小部分,也会把整个图集拉进内存中。 所以我们会将宽高超过128或者256的图从图集中拿出来,比如游戏中的一些玩家头像,背景图等等。
IOS上如果没有透明通道那么使用PVRTC来压缩必然是首选. 如果一些玩家的头像设计上就不是正方形,如果恰巧头像没有半透,那么使用ASTC岂不是浪费了。
由于图集肯定满足2的幂次方,所以我们将这张图变成一个单独的图集就可以进行正确的压缩了 而且图片不会模糊,如果使用Non Power of 2拉伸图片就会模糊。
注意
UGUI的事件本质上就是发送射线,由于UI的操作有一些复杂的手势,所以UGUI帮我们又封装了一层。 创建任意UI时都会自动创建EventSystem对象,并且绑定EventSystem.cs和StandaloneInputModule.cs如下代码所示,EventSystem会将该对象绑定的所有InputModule脚本收集起来保存在SystemInputModules对象中。
原因
当发生点击时,RaycasterManager.GetRaycasters();方法就是获取当前到底有多少个绑定GraphicRaycaster脚本的对象,那么同时参与点击事件的Canvas越多效率也就越低了.
由于多个UI有相交的情况,但由于Mesh都合批了第一个与射线相交的对象是没有意义的,但是我们只需要响应在最上面的UI元素,这里只能根据depth来做个排序了,找到最上面的UI元素,最后再抛出正确的点击事件。
游戏中有很多界面是叠在一起的,最上面的界面已经挡住了所有界面,但是由于下面的界面还有GraphicRaycaster对象,那么必然产生额外的计算开销.
所以说GraphicRaycaster组件越多越卡,raycastTarget勾选的越多越卡.
解决方案
优化工具
下面的代码可以在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())
{
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
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.
总结
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重建,可以观察具体瓶颈。
普通界面就像一般的背包装备界面,如果可以设计成全屏界面,那么还可以关掉3D摄像机。没有了3D部分的渲染,那么效率必然会有所提升。而且普通界面的UI一般就是按钮和滑动列表两种,此时的帧率即使掉到了25帧玩家也是可以接受的。但是战斗界面就不一样的,因为战斗期间帧率一旦掉到了30帧,玩家都能感受到,所以说战斗界面操作慢是完全不能忍受的。
战斗界面是战斗时玩家可操作的界面,也是游戏中最敏感的界面,只要有一点卡顿玩家就能很明显感知到,假设游戏战斗保持45FPS,那么留给每一帧的时间只有22ms,留给UI的只会更少。
非战斗界面比战斗界面的效率要求要低一些,因为界面几乎已经挡住战斗画面,单纯操作界面,保持在30FPS就可以了。