ZMUIL6——高性能系统

高性能系统要做的工作

高性能系统是为了解决UI性能问题,增加游戏的流畅度而设计的一系列性能解决方案。
主要针对渲染、重绘、顶点、UI组件、等多个方面进行性能的优化处理。
他的功能有以下几点:

  1. 一键优化合批。 自动根据图集图片和相邻组件的特征进行重新排序。
  2. 避免使用SetActive引起的UI重绘和GC垃圾。 用CanvasGroup和Scale进行代替。
  3. 使用UI对象池。 避免频繁的克隆物体导致的卡顿和GC。(在之前的UIModule​里已经实现了,隐藏窗口不会直接销毁窗口对象,可复用)
  4. 智能化禁用不必要的组件属性。 从而来避免一些不必要的性能开销。
  5. 界面预加载。 针对复杂一些的界面我们可以使用预加载进行提前加载物体,来确保在真正使用界面时,能够流畅度加载出界面。
  6. **高性能文字描边。**​Unity描边组件是拷贝4份相同的文本顶点数占用量巨大。
    一个字母的Text加上Untiy的描边一共占用30个顶点。
    而我们的Text同样是一个字母加上描边能做到只占用6个顶点。性能是Unity组件的5倍。
    (这课没讲,不用记辣)
  7. 组件自动序列化。 避免掉使用Find接口查找组件带来的性能消耗,而使用自动化序列化的方式拿到组件,将性能消耗尽可能的降至最低。
    (编写自动化系统时就已经实现了,使用组件数据脚本即可)

特点:最大的程度去降低UI在游戏中所消耗的性能问题,让我们做出来的游戏质量更好,游戏流畅度更高。

窗口预加载

就是将窗口的加载和窗口的弹出分离,在特定时间提前加载窗口而不显示它,达到预加载的结果,之后再显示窗口,使显示窗口更加顺畅

预加载在UIModule内实现,会调用窗口的OnAwake​方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//UIModule内
#region 预加载相关
/// <summary>
/// 预加载窗口,只加载物体,而不调用生命周期,也不显示它
/// </summary>
/// <typeparam name="T"></typeparam>
public void PreLoadWindow<T>() where T : WindowBase, new()
{
//获取类名
System.Type type = typeof(T);
string windowName = type.Name;
T windowBase = new T();
//加载对应的窗口预制体
GameObject newWindowObj = TempLoadWindow(windowName);
//初始化对应的管理类,直接加入到队列及字典内,而不进行排序以及生命周期的调用等
if (newWindowObj != null)
{
//初始化WindowBase各属性成员
windowBase.gameObject = newWindowObj;
windowBase.transform = newWindowObj.transform;
windowBase.Canvas = newWindowObj.GetComponent<Canvas>();
windowBase.Canvas.worldCamera = mUICamera;
windowBase.Name = newWindowObj.name;
windowBase.OnAwake();
windowBase.SetVisible(false);
//初始化该窗口的RectTransform,防止位置出现偏差
RectTransform rectTrans = newWindowObj.GetComponent<RectTransform>();
rectTrans.anchorMax = Vector2.one;
rectTrans.offsetMax = Vector2.zero;
rectTrans.offsetMin = Vector2.zero;
//添加到对应的列表
mAllWindowDic.Add(windowName, windowBase);
mAllWindowList.Add(windowBase);
}
Debug.Log($"预加载窗口:{windowName}");
}
#endregion

优化显隐逻辑

检测网格重建行为

可以通过下面的脚本直接检查网格重建行为,随意挂载到一个对象上即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;

public class CanvasRebuildTest : MonoBehaviour
{
IList<ICanvasElement> mLayoutRebuildQueue; //层级网格重建队列
IList<ICanvasElement> mGraphicRebuildQueue; //图形网格重建队列

void Start()
{
Type type = typeof(CanvasUpdateRegistry);
FieldInfo field = type.GetField("m_LayoutRebuildQueue", BindingFlags.NonPublic | BindingFlags.Instance);
mLayoutRebuildQueue = (IList<ICanvasElement>)field.GetValue(CanvasUpdateRegistry.instance);

field = type.GetField("m_GraphicRebuildQueue", BindingFlags.NonPublic | BindingFlags.Instance);
mGraphicRebuildQueue = (IList<ICanvasElement>)field.GetValue(CanvasUpdateRegistry.instance);
}

void Update()
{
for (int i = 0; i < mLayoutRebuildQueue.Count; i++)
{
var rebuild = mLayoutRebuildQueue[i];
if (ObjectValidForUpdate(rebuild))
{
Debug.LogFormat("{0}引起{1}网格重建", rebuild.transform.name, rebuild.transform.GetComponent<Graphic>().canvas.name);
}
}
for (int i = 0; i < mGraphicRebuildQueue.Count; i++)
{
var rebuild = mGraphicRebuildQueue[i];
if (ObjectValidForUpdate(rebuild))
{
Debug.LogFormat("{0}引起{1}网格重建", rebuild.transform.name, rebuild.transform.GetComponent<Graphic>().canvas.name);
}
}
}

/// <summary>
/// 检查帧更新对象是否有效
/// </summary>
/// <param name="element">Canvas元素</param>
/// <returns>是否有效</returns>
private bool ObjectValidForUpdate(ICanvasElement element)
{
var valid = element != null;
var isUnityObject = element is UnityEngine.Object;
if (isUnityObject)
{
valid = (element as object) != null;
}
return valid;
}
}

检查发现,直接使用SetActive​方法控制窗口的显隐会触发网格重建,导致不必要的性能消耗,
CanvasGroup​改变alpha值和改变Transform​的缩放不会触发网格重建

因此,我们可以调整CanvasGroup​改变alpha值和改变Transform​的缩放来控制UI对象的显示隐藏,性能更好

窗口的显隐

我们为每个窗口都添加一个CanvasGroup​,通过改变其alpha值为0或1,来控制窗口的隐藏或显示

WindowBase​的设置可见性方法改成调整CanvasGroup​的逻辑

1
2
3
4
5
6
7
8
9
10
//WindowBase
private CanvasGroup mCanvasGroup; //窗口的CanvasGroup

public override void SetVisible(bool isVisible)
{
//gameObject.SetActive(isVisible); //直接使用SetActive会造成UI控件重绘,浪费性能
mCanvasGroup.alpha = isVisible ? 1f : 0f; //使用CanvasGroup来控制UI窗口的显隐,不会造成UI控件重绘,性能更好
mCanvasGroup.blocksRaycasts = isVisible; //根据是否显示,决定是否接收用户输入,防止隐藏的窗口挡住显示的窗口输入
Visible = isVisible;
}

UI对象的显隐

我们可以编写一个UGUIAgent​,在其中编写一系列扩展方法,拓展一种调整缩放来控制显隐的方法,为UI控件的显隐提供新的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using UnityEngine;
using UnityEngine.UI;

public static class UGUIAgent
{
/// <summary>
/// 设置可见性,通过将缩放调整至0的方式来隐藏某个对象
/// </summary>
/// <param name="obj">要隐藏的对象</param>
/// <param name="visible">是否可见</param>
public static void SetVisible(this GameObject obj, bool visible)
{
obj.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
/// <summary>
/// 设置可见性,通过将缩放调整至0的方式来隐藏某个对象
/// </summary>
/// <param name="obj">要隐藏的对象</param>
/// <param name="visible">是否可见</param>
public static void SetVisible(this Transform transform, bool visible)
{
transform.localScale = visible ? Vector3.one : Vector3.zero;
}

public static void SetVisible(this Button button, bool visible)
{
button.transform.localScale = visible ? Vector3.one : Vector3.zero;
}

public static void SetVisible(this Text text, bool visible)
{
text.transform.localScale = visible ? Vector3.one : Vector3.zero;
}

public static void SetVisible(this Slider slider, bool visible)
{
slider.transform.localScale = visible ? Vector3.one : Vector3.zero;
}

public static void SetVisible(this Toggle toggle, bool visible)
{
toggle.transform.localScale = visible ? Vector3.one : Vector3.zero;
}

public static void SetVisible(this InputField inputField, bool visible)
{
inputField.transform.localScale = visible ? Vector3.one : Vector3.zero;
}

public static void SetVisible(this RawImage image, bool visible)
{
image.transform.localScale = visible ? Vector3.one : Vector3.zero;
}

public static void SetVisible(this ScrollRect scrollRect, bool visible)
{
scrollRect.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
}

智能禁用RaycastTarget

很多单纯的Image和Text是不接收用户输入的,因此将其RaycastTarget开着只能浪费性能
我们可以检测新创建出来的Text, Image, Raw Image,将其RaycastTarget属性自动关闭,省去我们手动关闭的麻烦
也不影响其他用于Button等的Image的射线检测正常执行

实现思路如下:

在编辑器模式下,每当Hierarchy窗口发生改变,就检测选中的物体(一般都是新创建的UI对象)名字是否包含Text, Image字样,一旦存在自动关闭其RaycastTarget属性

注:不对Text Mesh Pro生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;

public class SystemUIEditor : Editor
{
//该特性使得该方法可以在编辑器模式下自动调用
[InitializeOnLoadMethod]
private static void InitEditor()
{
//监听Hierarchy窗口发生改变的委托
EditorApplication.hierarchyChanged += HanderTextOrImageRaycast;
}

//监听新创建出来的Text, Image, Raw Image,将其RaycastTarget属性直接关闭(不支持TMP)
private static void HanderTextOrImageRaycast()
{
GameObject obj = Selection.activeGameObject;
if (obj != null)
{
if (obj.name.Contains("Text"))
{
Text text = obj.GetComponent<Text>();
if (text != null)
{
text.raycastTarget = false;
}
}
else if (obj.name.Contains("Image"))
{
Image image = obj.GetComponent<Image>();
if (image != null)
{
image.raycastTarget = false;
}
else
{
RawImage rawImage = obj.GetComponent<RawImage>();
if (rawImage != null)
{
rawImage.raycastTarget = false;
}
}
}
}
}

//该函数与上面的内容无关
private static void LoadWindowCamera()
{
if (Selection.activeGameObject != null)
{
GameObject uiCameraObj = GameObject.Find("UICamera");
if (uiCameraObj != null)
{
Camera camera = uiCameraObj.GetComponent<Camera>();
if (Selection.activeGameObject.name.Contains("Window"))
{
Canvas canvas = Selection.activeGameObject.GetComponent<Canvas>();
if (canvas != null)
{
canvas.worldCamera = camera;
}
}
}
}
}
}

一键自动优化Batchs

与NGUI的优化Batchs类似,它会将重新为同层级的对象进行排序,将同图集的对象排列到一起,将Text对象统一排列到末尾
以最大化的降低Batchs

通过导入UGUI-Editor这个开源库,即可使用该功能

image

image

image

悲报:
看了一眼这个UGUI-Editor在GitHub上的库,已经久未更新了,将该库导入到2021版的Unity出现了不少过时警告,但还是能用
使用其PrafabWin窗口也有轻微显示问题,笔者难以保证这个库的其他功能还能在未来的版本里正常使用