ZMUIL1——UI管理系统

UI管理系统要做的工作

  1. UI管理系统不仅要实现诸如控制UI窗口加载显隐等常见功能,还要实现独立于MonoBehaviour​之外的UI对象生命周期函数

  2. WindowBehaviour​​​基类

    WindowBehaviour​​​声明窗口的基础属性,如gameObject​​​、IsVisable​​​等,并声明生命周期函数,如OnAwake​​​等
    使本框架下的窗口对象在拥有类似于MonoBehaviour​​​的基础属性和生命周期函数的同时,我们可以自己控制UI窗口的生命周期函数的执行

  3. WindowBase​​​基类(继承WindowBehaviour​​​)

    将本框架下的UI窗口的部分共同功能抽象到该基类内,后续所有的Window脚本都继承于它
    WindowBase​​​会管理常用的UI控件,窗口的遮罩对象和所有UI控件的父节点对象。
    提供窗口初始化方法、显隐窗口方法、显隐遮罩方法,以及常用UI控件的监听事件添加与移除方法等

  4. UIMoudle​​​

    整个UI模块的核心,管理场景上的UI摄像机,UI根节点,UI配置文件,以及所有加载出来的UI窗口
    对外,​UIMoudle​提供窗口的加载,显隐,销毁等接口
    在以上功能中,UIModule​会在合适的时机执行WindowBehaviour​的不同的生命周期函数
    后续还要在UIModule​实现单遮调节遮罩逻辑,堆栈系统逻辑等

image

一、WindowBehaviour

WindowBehaviour​是所有Window窗口类的顶层类,为所有Window逻辑类提供基础属性和行为
UI窗口的基类WindowBase​就会继承WindowBehaviour

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
using System;
using UnityEngine;

public abstract class WindowBehaviour
{
//基础属性
public GameObject gameObject { get; set; } //当前窗口对象
public Transform transform { get; set; } //当前窗口对象的Transform
public Canvas Canvas { get; set; } //当前窗口对象的Canvas
public string Name { get; set; } //当前对象的名字
public bool Visible { get; set; } //窗口是否可见
//堆栈系统属性
public bool PopStack { get; set; } //是否是通过堆栈系统弹出的弹窗,若是,则处于堆栈弹出的流程时,隐藏该窗口将弹出下一个窗口
public Action<WindowBase> PopStackListener { get; set; } //从堆栈系统出栈时的监听函数

//生命周期函数
public virtual void OnAwake() { } //只会在窗口对象加载出来的时候执行一次,与MonoBehaviour的OnAwake作用一致
public virtual void OnShow() { } //在窗口显示的时候执行一次,与MonoBehaviour的OnEnable作用一致
public virtual void OnHide() { } //在窗口隐藏的时候执行一次,与MonoBehaviour的OnEnable作用一致
public virtual void OnDestroy() { } //在窗口对象被销毁时调用一次,与MonoBehaviour的OnDestroy作用一致

/// <summary>
/// 设置物体的可见性
/// </summary>
/// <param name="visible">是否可见</param>
public virtual void SetVisible(bool visible) { }
}

其中:

  • WindowBehaviour​实现的生命周期函数由UIModule​在合适的时机调用
  • PopStack​​及PopStackListener​​是堆栈系统会调用的属性

二、WindowBase

WindowBase​是所有Window逻辑类的基类,继承WindowBehaviour
当使用自动化系统为UI窗口对象生成Window逻辑类时,Window逻辑类会自动继承WindowBase
WindowBase​将所有UI窗口的部分共同功能抽象到该基类内,包括:

  • 管理常用的需要监听用户输入的UI控件,并对外提供监听事件管理接口
  • 重写WindowBehaviour​​​的生命周期函数,在其中执行UI窗口生成和销毁的必要逻辑,例如窗口初始化,执行显隐动画,清除监听事件等
  • 管理遮罩的CanvasGroup​​​,对外提供显隐遮罩的方法,供UIModule​​​的遮罩系统调用
  • 管理UI窗口通用显隐动画方法
  • 提供隐藏窗口方法(主要是派生类调用)和设置窗口可见性方法(UIModule​​​内调用)

1.窗口基本成员

WindowBase​会自行管理常用的UI控件button​、toggle​ 和 inputField​,UI控件会在添加监听事件时添加到管理列表内

注:调用添加监听事件的语句不需要由开发者编写,它们会在后续的自动化系统里由自动生成的脚本生成,也就是说,我们无需自己将UI控件加入到列表内

WindowBase​还会管理所有窗口的遮罩对象,UI控件父节点对象,以及窗口上自带的CanvasGroup

注:在本框架下,UI窗口对象默认都采用下图的结构:

image

UIMask是遮罩,UIContent是所有UI控件的父节点,采用这种结构的原因是为了方便后续遮罩系统管理遮罩显隐

本框架还默认窗口对象上挂载CanvasGroup​​,它用来替代SetActive​​来控制窗口显隐,因此需要管理它,原因在高性能系统里解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//常用UI控件管理列表
private List<Button> mAllButtonList = new List<Button>(); //所有Button的列表
private List<Toggle> mToggleList = new List<Toggle>(); //所有Toggle的列表
private List<InputField> mInputList = new List<InputField>(); //所有输入框的列表

private CanvasGroup mUIMask; //窗口的遮罩的CanvasGroup
private CanvasGroup mCanvasGroup; //窗口的CanvasGroup
protected Transform mUIContent; //窗口的UI控件父节点
protected bool mDisableAnim = false; //是否禁用动画

//初始化基类组件
private void InitializeBaseComponent()
{
mCanvasGroup = transform.GetComponent<CanvasGroup>();
mUIMask = transform.Find("UIMask").GetComponent<CanvasGroup>();
mUIContent = transform.Find("UIContent").transform;
}

这里的初始化窗口方法会在基类的OnAwake()​里调用

2.监听事件管理相关

WindowBase​封装了Button​、Toggle​、InputField​这三种常用控件的添加监听事件方法,该方法需要传入控件本身与监听方法
若控件列表内没有这个控件会将控件加入管理列表,然后清空控件原来的监听方法,重新添加传入的监听方法
该方法会在自动化系统生成的组件数据脚本/组件数据脚本调用,用于初始化UI窗口的控件事件监听(也就是说,我们不需要自己初始化组件监听方法)

WindowBase​​还封装了Button​​、Toggle​​、InputField​​这三种常用控件的清除事件监听方法,
该方法可以一次清空所有管理的控件的监听方法,它会在窗口销毁时会调用

三种控件的添加监听方法与清除监听方法的逻辑大同小异

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
//WindowBase内,以Button为例
private List<Button> mAllButtonList = new List<Button>(); //所有Button的列表
private List<Toggle> mToggleList = new List<Toggle>(); //所有Toggle的列表
private List<InputField> mInputList = new List<InputField>(); //所有输入框的列表

public void AddButtonClickListener(Button button, UnityAction action)
{
if (button != null)
{
if (!mAllButtonList.Contains(button))
{
mAllButtonList.Add(button);
}
button.onClick.RemoveAllListeners();
button.onClick.AddListener(action);
}
}

public void RemoveAllButtonListener()
{
foreach (var button in mAllButtonList)
{
button.onClick.RemoveAllListeners();
}
}

//其他两种控件逻辑大同小异
public void AddToggleClickListener(Toggle toggle, UnityAction<bool, Toggle> action)
public void AddInputFieldListener(InputField input, UnityAction<string> onChangeAction, UnityAction<string> endAction)

3.通用显隐动画相关

WindowBase​​通过调用DOTween的拓展方法,实现窗口通用的的显示隐藏动画方法,并提供mDisableAnim​​变量来决定是否调用这些动画
开发者可以在这两个方法内覆写其他代码来实现不同的效果,也可以在派生类里的OnAwake()​​里将mDisableAnim​​赋值为true​​以禁用动画

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
//WindowBase内
protected bool mDisableAnim = false; //是否禁用动画

//窗口显示时会播放的动画
public void ShowAnimation()
{
if (Canvas.sortingOrder > 90 && mDisableAnim == false)
{
//Mask动画
mUIMask.alpha = 0f;
mUIMask.DOFade(1, 0.2f);
//缩放动画
mUIContent.localScale = Vector3.one * 0.8f;
//表示Scale从0.8缩放到1,消耗0.3s时间,使用OutBack曲线
mUIContent.DOScale(Vector3.one, 0.3f).SetEase(Ease.OutBack);
}
}

//窗口隐藏时会播放的动画
public void HideAnimation()
{
if (Canvas.sortingOrder > 90 && mDisableAnim == false)
{
mUIContent.DOScale(Vector3.one * 1.1f, 0.2f).SetEase(Ease.OutBack).OnComplete(() =>
{
UIModule.Instance.HideWindow(Name);
});
}
else
{
UIModule.Instance.HideWindow(Name);
}
}

禁用动画,将mDisableAnim​设为true​即可

1
2
3
4
5
6
7
8
//UserInfoWindow,继承WindowBase
public override void OnAwake()
{
dataComponent = gameObject.GetComponent<UserInfoWindowDataComponent>();
dataComponent.InitComponent(this);
mDisableAnim = true; //这样窗口显隐将不播放动画
base.OnAwake();
}

4.生命周期函数的重写

主要是在OnAwake()​里执行初始化方法,OnDestroy()​清除事件监听并释放内存

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
#region 生命周期函数的声明
public override void OnAwake()
{
base.OnAwake();
InitializeBaseComponent();
}

public override void OnShow()
{
base.OnShow();
ShowAnimation();
}

public override void OnHide()
{
base.OnHide();
}

public override void OnDestroy()
{
base.OnDestroy();
RemoveAllButtonListener();
RemoveAllToggleListener();
RemoveAllInputListener();
mAllButtonList.Clear();
mToggleList.Clear();
mInputList.Clear();
}
#endregion

WindowBase代码

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
using DG.Tweening;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class WindowBase : WindowBehaviour
{
//常用UI控件管理列表
private List<Button> mAllButtonList = new List<Button>(); //所有Button的列表
private List<Toggle> mToggleList = new List<Toggle>(); //所有Toggle的列表
private List<InputField> mInputList = new List<InputField>(); //所有输入框的列表

private CanvasGroup mUIMask; //窗口的遮罩的CanvasGroup
private CanvasGroup mCanvasGroup; //窗口的CanvasGroup
protected Transform mUIContent; //窗口的UI控件父节点
protected bool mDisableAnim = false; //是否禁用动画

//初始化基类组件
private void InitializeBaseComponent()
{
mCanvasGroup = transform.GetComponent<CanvasGroup>();
mUIMask = transform.Find("UIMask").GetComponent<CanvasGroup>();
mUIContent = transform.Find("UIContent").transform;
}

#region 生命周期函数的声明
public override void OnAwake()
{
base.OnAwake();
InitializeBaseComponent();
}

public override void OnShow()
{
base.OnShow();
ShowAnimation();
}

public override void OnHide()
{
base.OnHide();
}

public override void OnDestroy()
{
base.OnDestroy();
RemoveAllButtonListener();
RemoveAllToggleListener();
RemoveAllInputListener();
mAllButtonList.Clear();
mToggleList.Clear();
mInputList.Clear();
}
#endregion

#region 动画管理相关
public void ShowAnimation()
{
if (Canvas.sortingOrder > 90 && mDisableAnim == false)
{
//Mask动画
mUIMask.alpha = 0f;
mUIMask.DOFade(1, 0.2f);
//缩放动画
mUIContent.localScale = Vector3.one * 0.8f;
//表示Scale从0.8缩放到1,消耗0.3s时间,使用OutBack曲线
mUIContent.DOScale(Vector3.one, 0.3f).SetEase(Ease.OutBack);
}
}

public void HideAnimation()
{
if (Canvas.sortingOrder > 90 && mDisableAnim == false)
{
mUIContent.DOScale(Vector3.one * 1.1f, 0.2f).SetEase(Ease.OutBack).OnComplete(() =>
{
UIModule.Instance.HideWindow(Name);
});
}
else
{
UIModule.Instance.HideWindow(Name);
}
}
#endregion

public void HideWindow()
{
HideAnimation();
//UIModule.Instance.HideWindow(Name);
}

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

/// <summary>
/// 控制遮罩是否生效,仅在单遮模式开启时有效
/// </summary>
/// <param name="isVisible">是否可见</param>
public void SetMaskVisible(bool isVisible)
{
//如果未开启单遮模式,则直接返回
if (!UISetting.Instance.SINGMASK_SYSTEM) return;
mUIMask.alpha = isVisible ? 1f : 0f;
}

#region 事件管理方法
/// <summary>
/// 添加按钮的点击事件监听,添加后会清除原有监听函数
/// </summary>
/// <param name="button">要添加点击事件监听的按钮</param>
/// <param name="action">监听到按钮点击后执行什么事件</param>
public void AddButtonClickListener(Button button, UnityAction action)
{
if (button != null)
{
if (!mAllButtonList.Contains(button))
{
mAllButtonList.Add(button);
}
button.onClick.RemoveAllListeners();
button.onClick.AddListener(action);
}
}

public void AddToggleClickListener(Toggle toggle, UnityAction<bool, Toggle> action)
{
if (toggle != null)
{
if (!mToggleList.Contains(toggle))
{
mToggleList.Add(toggle);
}
toggle.onValueChanged.RemoveAllListeners();
toggle.onValueChanged.AddListener((isOn) =>
{
action?.Invoke(isOn, toggle);
});
}
}

public void AddInputFieldListener(InputField input, UnityAction<string> onChangeAction, UnityAction<string> endAction)
{
if (input != null)
{
if (!mInputList.Contains(input))
{
mInputList.Add(input);
}
input.onValueChanged.RemoveAllListeners();
input.onEndEdit.RemoveAllListeners();
input.onValueChanged.AddListener(onChangeAction);
input.onEndEdit.AddListener(endAction);
}
}

public void RemoveAllButtonListener()
{
foreach (var button in mAllButtonList)
{
button.onClick.RemoveAllListeners();
}
}

public void RemoveAllToggleListener()
{
foreach (var toggle in mToggleList)
{
toggle.onValueChanged.RemoveAllListeners();
}
}

public void RemoveAllInputListener()
{
foreach (var input in mInputList)
{
input.onValueChanged.RemoveAllListeners();
input.onEndEdit.RemoveAllListeners();
}
}
#endregion
}

三、UIModule

UIModule​是框架的核心,是所有UI窗口的单例管理器,它管理场景上唯一的UI摄像机,所有窗口。框架的多个系统都会在这里实现
UIModule​要提供的功能包括:

  • 窗口管理:管理场景上所有的Window逻辑类,以窗口类型和窗口名为参数,提供窗口的预加载,弹出,初始化,隐藏,销毁功能
  • 窗口生命函数的执行:当窗口加载、显隐、销毁时,UIModule​​需要调用执行该窗口对应的生命周期函数
  • 控制窗口遮罩的显隐:遮罩系统在这里实现,当开启单遮模式时,每次执行窗口显隐逻辑时,都会重新计算哪个窗口的遮罩需要开启,以实现单遮效果
  • 窗口按顺序依次显示:堆栈系统在这里实现,可以设置窗口弹出队列,让窗口依次弹出,期间可以追加窗口或清除队列,打开队列外窗口也不影响显示队列

1.UIModule的基础成员

UIModule​是管理器,因此采用单例模式
UIModule​管理场景上的UI摄像机,窗口父对象UIRoot,
UIModule​还会管理所有加载出来的窗口,以及显示中的窗口

WindowConfig​与自动化系统的自动获取UI预设体加载路径有关
mWindowStack​和mStartPopStackWindowStatus​则与堆栈系统有关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static UIModule _instance;
public static UIModule Instance
{
get
{
if (_instance == null)
{
_instance = new UIModule();
}
return _instance;
}
}

private Camera mUICamera; //场景上的UI摄像机
private Transform mUIRoot; //场景上的UI根节点
private WindowConfig mWindowConfig; //窗口读取路径配置文件

private Dictionary<string, WindowBase> mAllWindowDic = new Dictionary<string, WindowBase>(); //所有窗口的字典
private List<WindowBase> mAllWindowList = new List<WindowBase>(); //所有窗口的列表
private List<WindowBase> mVisibleWindowList = new List<WindowBase>(); //所有可见窗口的列表

private Queue<WindowBase> mWindowStack = new Queue<WindowBase>(); //堆栈系统的队列,用来管理弹窗的循环弹出
private bool mStartPopStackWindowStatus = false; //开始弹出堆栈的标志,用于处理多种情况,例如:正在出栈中有其他界面弹出,可以直接放到栈内进行弹出等

2.UIModule的初始化

UIModule第一次调用时不会初始化,需要自己进行初始化
初始化方法是通过Find​方法获取场景上的摄像机,UIRoot对象,加载并获取所有预设体组件加载路径

mWindowConfig​与mWindowConfig.GeneratorWindowConfig()​与自动化系统的自动获取UI预设体加载路径有关

1
2
3
4
5
6
7
8
9
10
11
12
//初始化方法 
public void Initialize()
{
//获取场景上的UI摄像机和UI根节点
mUICamera = GameObject.Find("UICamera").GetComponent<Camera>();
mUIRoot = GameObject.Find("UIRoot").transform;
mWindowConfig = Resources.Load<WindowConfig>("WindowConfig");
//打包出去后不会触发调用
#if UNITY_EDITOR
mWindowConfig.GeneratorWindowConfig();
#endif
}

3.UIModule的UI管理

UIModule​是使用WindowBase​来进行窗口管理的,而不使用WindowBehaviour​来管理,
因为WindowBehaviour​作为最顶层的类,缺乏WindowBase​的派生实现,
WindowBase​内会实现更多通用的功能,例如弹出与消失动画, 遮罩处理等,
使用WindowBase​管理能够更方便的调用WindowBase​接口

1
2
3
4
5
6
7
private Camera mUICamera;               //场景上的UI摄像机
private Transform mUIRoot; //场景上的UI根节点
private WindowConfig mWindowConfig; //窗口读取路径配置文件

private Dictionary<string, WindowBase> mAllWindowDic = new Dictionary<string, WindowBase>(); //所有窗口的字典
private List<WindowBase> mAllWindowList = new List<WindowBase>(); //所有窗口的列表
private List<WindowBase> mVisibleWindowList = new List<WindowBase>(); //所有可见窗口的列表

a) 窗口弹出

弹出窗口会触发窗口的OnShow()​方法,如果是第一次弹出,还会触发OnAwake​方法
弹出窗口让遮罩系统重新计算单遮

UIModule​的窗口弹出实现较繁琐,因此在这里记录,这里使用伪代码阐述思路:

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
//弹出窗口伪代码,演示弹出窗口运行思路,实际代码会分成多个函数

//弹出窗口方法的调用,需要传入要显示的窗口类,窗口类必须继承WindowBase
要显示的窗口类 窗口对象 = UIMoudle.Instance.PopUpWindow<要显示的窗口类>();

//弹出窗口方法的实现,T是继承WindowBase的窗口类
public T PopUpWindow<T>() : where T : WindowBase, new()
{
窗口名 = 通过传入的T获取类名
if 通过窗口名发现字典内存在窗口 //窗口存在于内存内,执行显示窗口逻辑
T t = 通过窗口名从字典获取窗口
if t.gameObject不为空 && t不可见
将t加入到可见窗口列表
将t.transform移到同层级(兄弟窗口)的最后一个位置
设置t可见,执行t的OnShow方法
计算开启哪个窗口的遮罩
return t
else
报错
else //窗口不存在于内存内,需要实例化并重新初始化
T t = new T()
//生成对应的窗口预制体
GameObject UI对象 = 通过窗口名从硬盘内加载预制体 //使用Resources,AB包,或使用自己的加载框架皆可
if UI对象 != null
初始化t的各个基础属性,将UI对象及其Canvas和UI摄像机关联到t上,执行t的OnAwake方法
将窗口对象移到同层级(兄弟窗口)的最后一个位置
设置t可见,执行t的OnShow方法
初始化t的RectTransform
将t加入到字典与列表内
计算开启哪个窗口的遮罩
return t
else
报错
return null
}

代码具体实现

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#region 弹出窗口相关
// 弹出一个窗口
public T PopUpWindow<T>() where T : WindowBase, new()
{
//获取类名
System.Type type = typeof(T);
string windowName = type.Name;
//尝试通过类名从内存中获取已加载过的WindowBase
WindowBase window = GetWindow(windowName);
//如果该类已经加载到内存(存在于字典内),直接调用显示方法并返回
if (window != null)
{
return ShowWindow(windowName) as T;
}
//如果该类未加载(不存在于字典内),也就是第一次弹出,则先创建并初始化再返回
T t = new T();
return InitializeWindow(t, windowName) as T;
}

private WindowBase InitializeWindow(WindowBase windowBase, string windowName)
{
//生成对应的窗口预制体
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.transform.SetAsLastSibling();
//调用OnAwake()生命周期函数,设置其可见,再调用OnShow()
windowBase.OnAwake();
windowBase.SetVisible(true);
windowBase.OnShow();
//初始化该窗口的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);
mVisibleWindowList.Add(windowBase);
SetWindowMaskVisible();
return windowBase;
}
Debug.LogError("未加载到对应的窗口,窗口名:" + windowName);
return null;
}

//通过名字显示对应Window方法
private WindowBase ShowWindow(string windowName)
{
WindowBase window = null;
//先判断字典是否存在该窗口
if (mAllWindowDic.ContainsKey(windowName))
{
window = mAllWindowDic[windowName];
//若窗口存在且不可见,先将窗口加入到可见窗口的列表,将其调整到同层级的最后一个,再调用窗口显示方法
if (window.gameObject != null && window.Visible == false)
{
mVisibleWindowList.Add(window);
window.transform.SetAsLastSibling();
window.SetVisible(true);
SetWindowMaskVisible();
window.OnShow();
}
return window;
}
//若不存在,打印错误信息并返回null
else
Debug.LogError(windowName + ":窗口不存在!请调用PopUpWindow进行弹出");
return null;
}

//从mAllWindowDic内通过名字获取已加载过的WindowBase
private WindowBase GetWindow(string windowName)
{
if (mAllWindowDic.ContainsKey(windowName))
{
return mAllWindowDic[windowName];
}
return null;
}
#endregion

b) 资源加载相关

值得一提的是,在初始化方法内,为了教学方便,临时使用Resources​的方法来加载预设体
实际使用时,可以使用自己的加载框架加载,或者使用更好的加载方案,因此,这里将加载预设体的代码封装为一个临时方法,方便未来替换

这里的mWindowConfig.GetWindowPath(windowName)​​是通过继承ScriptableObject​​的配置文件获取该窗口UI预设体的加载路径
这部分在自动化系统内讲解

1
2
3
4
5
6
7
8
9
10
11
//TODO... 临时资源加载,这里仅仅是因为教程方便而使用Resources加载,如有自己的资源加载框架,或后续学习使用加载框架,此处代码将会修改!
public GameObject TempLoadWindow(string windowName)
{
GameObject window = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>(mWindowConfig.GetWindowPath(windowName)), mUIRoot);
//window.transform.SetParent(mUIRoot);
window.transform.localScale = Vector3.one;
window.transform.localPosition = Vector3.zero;
window.transform.rotation = Quaternion.identity;
window.name = windowName;
return window;
}

c) 获取窗口

GetWindow​有两种重载,一种是内部私有方法,参数为窗口名,一个提供给外部,参数为泛型
对外接口只能获取显示中的窗口

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
//从mAllWindowDic内通过名字获取已加载过的WindowBase
private WindowBase GetWindow(string windowName)
{
if (mAllWindowDic.ContainsKey(windowName))
{
return mAllWindowDic[windowName];
}
return null;
}

/// <summary>
/// 获取已经弹出的窗口
/// </summary>
/// <typeparam name="T">要获取的窗口类</typeparam>
/// <returns>获取到的窗口类</returns>
public T GetWindow<T>() where T: WindowBase
{
System.Type type = typeof(T);
foreach (var window in mVisibleWindowList)
{
if (window.Name == type.Name)
{
return (T)window;
}
}
Debug.LogError("该窗口未获取到:" + type.Name);
return null;
}

d) 隐藏窗口

HideWindow​有三种重载,内部私有方法的参数为窗口对象,核心逻辑也在这里实现,另外两个提供给外部,参数为泛型或窗口名
隐藏窗口会触发窗口的OnHide()​方法
隐藏窗口让遮罩系统重新计算单遮,如果还是通过堆栈系统弹出的窗口,堆栈系统将会弹出队列中的下一个窗口

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
#region 隐藏窗口相关
/// <summary>
/// 隐藏窗口
/// </summary>
/// <typeparam name="T">要隐藏的窗口类</typeparam>
// 这个重载函数作为对外调用接口使用
public void HideWindow<T>() where T : WindowBase
{
HideWindow(typeof(T).Name);
}

//这个重载函数的作用是将通过传入的窗口名从字典中获取窗口
public void HideWindow(string windowName)
{
WindowBase window = GetWindow(windowName);
HideWindow(window);
}

//这个重载函数的作用是判断窗口是否符合条件再执行隐藏逻辑
private void HideWindow(WindowBase window)
{
//若Window不为空且可见,就执行隐藏逻辑
if (window != null && window.Visible)
{
mVisibleWindowList.Remove(window);
window.SetVisible(false); //隐藏弹窗对象
SetWindowMaskVisible();
window.OnHide(); //执行生命周期函数
}
PopNextStackWindow(window); //如果处于出栈的情况下,上一个界面隐藏时,自动打开栈中的下一个界面
}
#endregion

e) 销毁窗口

DesttoyWindow​有三种重载,内部私有方法的参数为窗口对象,核心逻辑也在这里实现,另外一个提供给外部,参数为泛型
销毁窗口会触发窗口的OnHide()​和OnDestory()​方法
销毁窗口让遮罩系统重新计算单遮,如果还是通过堆栈系统弹出的窗口,堆栈系统将会弹出队列中的下一个窗口

销毁窗口还有一个销毁全部弹窗的方法,用于过场景时是否内存

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
#region 销毁窗口相关
/// <summary>
/// 销毁窗口
/// </summary>
/// <typeparam name="T">要销毁的窗口的方法</typeparam>
// 这个重载函数作为对外调用接口使用
public void DestroyWindow<T>() where T : WindowBase
{
DestroyWindow(typeof(T).Name);
}

//这个重载函数的作用是将通过传入的窗口名从字典中获取窗口
private void DestroyWindow(string windowName)
{
WindowBase window = GetWindow(windowName);
DestroyWindow(window);
}

//这个重载函数的作用是执行销毁逻辑
private void DestroyWindow(WindowBase window)
{
if (window != null)
{
if (mAllWindowDic.ContainsKey(window.Name))
{
mAllWindowDic.Remove(window.Name);
mAllWindowList.Remove(window);
mVisibleWindowList.Remove(window);
}
window.SetVisible(false);
SetWindowMaskVisible();
window.OnHide();
window.OnDestroy();
GameObject.Destroy(window.gameObject);
PopNextStackWindow(window); //如果处于出栈的情况下,上一个界面隐藏时,自动打开栈中的下一个界
}
}

/// <summary>
/// 销毁所有弹窗,并释放内存
/// </summary>
/// <param name="filterlist">过滤列表</param>
public void DestroyAllWindow(List<string> filterlist = null)
{
//使用反向for循环来销毁弹窗,从最后一个开始销毁,防止越界
for (int i = mAllWindowList.Count - 1; i >= 0; i--)
{
WindowBase window = mAllWindowList[i];
if (window == null || (filterlist != null && filterlist.Contains(window.Name)))
{
continue;
}
DestroyWindow(window.Name);
}
Resources.UnloadUnusedAssets();
}
#endregion

UIModule代码

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIModule
{
private static UIModule _instance;
public static UIModule Instance
{
get
{
if (_instance == null)
{
_instance = new UIModule();
}
return _instance;
}
}

private Camera mUICamera; //场景上的UI摄像机
private Transform mUIRoot; //场景上的UI根节点
private WindowConfig mWindowConfig; //窗口读取路径配置文件

private Dictionary<string, WindowBase> mAllWindowDic = new Dictionary<string, WindowBase>(); //所有窗口的字典
private List<WindowBase> mAllWindowList = new List<WindowBase>(); //所有窗口的列表
private List<WindowBase> mVisibleWindowList = new List<WindowBase>(); //所有可见窗口的列表

private Queue<WindowBase> mWindowStack = new Queue<WindowBase>(); //堆栈系统的队列,用来管理弹窗的循环弹出
private bool mStartPopStackWindowStatus = false; //开始弹出堆栈的标志,用于处理多种情况,例如:正在出栈中有其他界面弹出,可以直接放到栈内进行弹出等


//初始化方法
public void Initialize()
{
//获取场景上的UI摄像机和UI根节点
mUICamera = GameObject.Find("UICamera").GetComponent<Camera>();
mUIRoot = GameObject.Find("UIRoot").transform;
mWindowConfig = Resources.Load<WindowConfig>("WindowConfig");
//打包出去后不会触发调用
#if UNITY_EDITOR
mWindowConfig.GeneratorWindowConfig();
#endif
}

#region 窗口管理相关

#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

#region 弹出窗口相关
/// <summary>
/// 弹出一个窗口
/// </summary>
/// <typeparam name="T">弹出窗口的对应的WindowBase类</typeparam>
/// <returns>弹出窗口对应的WindowBase类</returns>
public T PopUpWindow<T>() where T : WindowBase, new()
{
//获取类名
System.Type type = typeof(T);
string windowName = type.Name;
//尝试通过类名从内存中获取已加载过的WindowBase
WindowBase window = GetWindow(windowName);
//如果该类已经加载到内存(存在于字典内),直接调用显示方法并返回
if (window != null)
{
return ShowWindow(windowName) as T;
}
//如果该类未加载(不存在于字典内),也就是第一次弹出,则先创建并初始化再返回
T t = new T();
return InitializeWindow(t, windowName) as T;
}

//堆栈系统专用弹出窗口方法,传入从堆栈系统弹出的WindowBase
private WindowBase PopUpWindow(WindowBase QueuePopWindow)
{
System.Type type = QueuePopWindow.GetType();
string windowName = type.Name;
//先确认是否加载过Window,若加载过,直接返回加载过的Window
WindowBase LoadedWindow = GetWindow(windowName);
if (LoadedWindow != null)
{
return ShowWindow(windowName);
}
//若未加载,则将弹出的WindowBase用于初始化
return InitializeWindow(QueuePopWindow, windowName);
}

private WindowBase InitializeWindow(WindowBase windowBase, string windowName)
{
//生成对应的窗口预制体
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.transform.SetAsLastSibling();
//调用OnAwake()生命周期函数,设置其可见,再调用OnShow()
windowBase.OnAwake();
windowBase.SetVisible(true);
windowBase.OnShow();
//初始化该窗口的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);
mVisibleWindowList.Add(windowBase);
SetWindowMaskVisible();
return windowBase;
}
Debug.LogError("未加载到对应的窗口,窗口名:" + windowName);
return null;
}

//通过名字显示对应Window方法
private WindowBase ShowWindow(string windowName)
{
WindowBase window = null;
//先判断字典是否存在该窗口
if (mAllWindowDic.ContainsKey(windowName))
{
window = mAllWindowDic[windowName];
//若窗口存在且不可见,先将窗口加入到可见窗口的列表,将其调整到同层级的最后一个,再调用窗口显示方法,执行OnShow()
if (window.gameObject != null && window.Visible == false)
{
mVisibleWindowList.Add(window);
window.transform.SetAsLastSibling();
window.SetVisible(true);
SetWindowMaskVisible();
window.OnShow();
}
return window;
}
//若不存在,打印错误信息并返回null
else
Debug.LogError(windowName + ":窗口不存在!请调用PopUpWindow进行弹出");
return null;
}

//从mAllWindowDic内通过名字获取已加载过的WindowBase
private WindowBase GetWindow(string windowName)
{
if (mAllWindowDic.ContainsKey(windowName))
{
return mAllWindowDic[windowName];
}
return null;
}
#endregion

#region 获取窗口相关

/// <summary>
/// 获取已经弹出的窗口
/// </summary>
/// <typeparam name="T">要获取的窗口类</typeparam>
/// <returns>获取到的窗口类</returns>
public T GetWindow<T>() where T: WindowBase
{
System.Type type = typeof(T);
foreach (var window in mVisibleWindowList)
{
if (window.Name == type.Name)
{
return (T)window;
}
}
Debug.LogError("该窗口未获取到:" + type.Name);
return null;
}
#endregion

#region 隐藏窗口相关
/// <summary>
/// 隐藏窗口
/// </summary>
/// <typeparam name="T">要隐藏的窗口类</typeparam>
// 这个重载函数作为对外调用接口使用
public void HideWindow<T>() where T : WindowBase
{
HideWindow(typeof(T).Name);
}

//这个重载函数的作用是将通过传入的窗口名从字典中获取窗口
public void HideWindow(string windowName)
{
WindowBase window = GetWindow(windowName);
HideWindow(window);
}

//这个重载函数的作用是判断窗口是否符合条件再执行隐藏逻辑
private void HideWindow(WindowBase window)
{
//若Window不为空且可见,就执行隐藏逻辑
if (window != null && window.Visible)
{
mVisibleWindowList.Remove(window);
window.SetVisible(false); //隐藏弹窗对象
SetWindowMaskVisible();
window.OnHide(); //执行生命周期函数
}
PopNextStackWindow(window); //如果处于出栈的情况下,上一个界面隐藏时,自动打开栈中的下一个界面
}
#endregion

#region 销毁窗口相关
/// <summary>
/// 销毁窗口
/// </summary>
/// <typeparam name="T">要销毁的窗口的方法</typeparam>
// 这个重载函数作为对外调用接口使用
public void DestroyWindow<T>() where T : WindowBase
{
DestroyWindow(typeof(T).Name);
}

//这个重载函数的作用是将通过传入的窗口名从字典中获取窗口
private void DestroyWindow(string windowName)
{
WindowBase window = GetWindow(windowName);
DestroyWindow(window);
}

//这个重载函数的作用是执行销毁逻辑
private void DestroyWindow(WindowBase window)
{
if (window != null)
{
if (mAllWindowDic.ContainsKey(window.Name))
{
mAllWindowDic.Remove(window.Name);
mAllWindowList.Remove(window);
mVisibleWindowList.Remove(window);
}
window.SetVisible(false);
SetWindowMaskVisible();
window.OnHide();
window.OnDestroy();
GameObject.Destroy(window.gameObject);
PopNextStackWindow(window); //如果处于出栈的情况下,上一个界面隐藏时,自动打开栈中的下一个界面
}
}

/// <summary>
/// 销毁所有弹窗,并释放内存
/// </summary>
/// <param name="filterlist">过滤列表</param>
public void DestroyAllWindow(List<string> filterlist = null)
{
//使用反向for循环来销毁弹窗,从最后一个开始销毁,防止越界
for (int i = mAllWindowList.Count - 1; i >= 0; i--)
{
WindowBase window = mAllWindowList[i];
if (window == null || (filterlist != null && filterlist.Contains(window.Name)))
{
continue;
}
DestroyWindow(window.Name);
}
Resources.UnloadUnusedAssets();
}
#endregion

#endregion

#region 控制遮罩相关
//单遮模式下,设置窗口遮罩
private void SetWindowMaskVisible()
{
//如果未开启单遮模式,则直接返回
if (!UISetting.Instance.SINGMASK_SYSTEM) return;
WindowBase maxOrderWindowBase = null; //渲染层级最大的窗口
int maxOrder = 0; //渲染层级最大窗口的渲染层级
int maxIndex = 0; //最大排序下标,在相同父节点下的位置下标
//1. 关闭所有窗口的Mask,设置为不可见
//2. 从所有可见窗口中,找到一个层级最大的窗口,设置Mask为可见
for (int i = 0; i < mVisibleWindowList.Count; i++)
{
WindowBase window = mVisibleWindowList[i];
//当窗口管理类不为空且游戏对象不为空时
if (window != null && window.gameObject != null)
{
//先关闭遮罩
window.SetMaskVisible(false);
if (maxOrderWindowBase == null)
{
maxOrderWindowBase = window;
maxOrder = window.Canvas.sortingOrder;
maxIndex = window.transform.GetSiblingIndex();
}
else
{
//找到最大渲染层级的窗口,获取它
if (maxOrder < window.Canvas.sortingOrder)
{
maxOrderWindowBase = window;
maxOrder = window.Canvas.sortingOrder;
}
//如果两个窗口的渲染层级相同,就找到同节点下最靠下的物体,优先渲染这个最靠下的Mask
else if (maxOrder == window.Canvas.sortingOrder && maxIndex < window.transform.GetSiblingIndex())
{
maxOrderWindowBase = window;
maxIndex = window.transform.GetSiblingIndex();
}
}
}
}
//遍历完所有窗口后,得到层级最大且同节点最靠下的窗口,只开启这一个最大的窗口即可
maxOrderWindowBase?.SetMaskVisible(true);
}
#endregion

//TODO... 临时资源加载,这里仅仅是因为教程方便而使用Resources加载,如有自己的资源加载框架,或后续学习使用加载框架,此处代码将会修改!
public GameObject TempLoadWindow(string windowName)
{
GameObject window = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>(mWindowConfig.GetWindowPath(windowName)), mUIRoot);
//window.transform.SetParent(mUIRoot);
window.transform.localScale = Vector3.one;
window.transform.localPosition = Vector3.zero;
window.transform.rotation = Quaternion.identity;
window.name = windowName;
return window;
}

#region 堆栈系统相关
/// <summary>
/// 向堆栈压入一个界面
/// </summary>
/// <typeparam name="T">要压入的窗口</typeparam>
/// <param name="popCallBack">出栈时要执行的监听函数</param>
public void PushWindowToStack<T>(Action<WindowBase> popCallBack = null) where T : WindowBase, new()
{
//这里的new出来的T暂时用于在队列内记录监听函数,
//后续出栈显示窗口时,若T已经加载过,则这里的监听函数将赋值给已经加载过的T,若未加载过再使用该T用于初始化
T windowBase = new T();
windowBase.PopStackListener = popCallBack;
mWindowStack.Enqueue(windowBase);
}

/// <summary>
/// 弹出堆栈中第一个弹窗
/// </summary>
public void StartPopFirstStackWindow()
{
if (mStartPopStackWindowStatus) return;
mStartPopStackWindowStatus = true; //表示已经开始进行堆栈弹出的流程
PopStackWindow();
}

/// <summary>
/// 压入并直接开始弹出堆栈弹窗
/// </summary>
/// <typeparam name="T">要压入的窗口</typeparam>
/// <param name="popCallBack">出栈时要执行的监听函数</param>
public void PushAndPopStackWindow<T>(Action<WindowBase> popCallBack = null) where T : WindowBase, new()
{
PushWindowToStack<T>(popCallBack);
StartPopFirstStackWindow();
}

/// <summary>
/// 弹出下一个窗口
/// </summary>
/// <param name="windowBase"></param>
private void PopNextStackWindow(WindowBase windowBase)
{
if (windowBase != null && mStartPopStackWindowStatus && windowBase.PopStack)
{
windowBase.PopStack = false;
PopStackWindow();
}
}

/// <summary>
/// 弹出堆栈弹窗
/// </summary>
/// <returns>是否从堆栈里弹出窗口</returns>
public bool PopStackWindow()
{
if (mWindowStack.Count > 0)
{
WindowBase recordWindow = mWindowStack.Dequeue();
WindowBase popWindow = PopUpWindow(recordWindow);
popWindow.PopStackListener = recordWindow.PopStackListener;
popWindow.PopStack = true; //表示是从堆栈系统里打开的窗口,关闭该窗口将重新执行这里的方法
popWindow.PopStackListener?.Invoke(popWindow);
popWindow.PopStackListener = null;
return true;
}
else
{
mStartPopStackWindowStatus = false; //表示堆栈弹出的流程结束
return false;
}
}

/// <summary>
/// 清空窗口堆栈,可用于中途取消堆栈弹出流程
/// </summary>
public void ClearStackWindows()
{
mWindowStack.Clear();
}
#endregion
}