8.UI模块
8.1 知识点
UI模块的作用
在游戏开发中,UI管理是一个非常重要的环节。我们经常需要创建大量的UI面板,每个面板都需要:
- 拼面板:在Unity编辑器中搭建UI界面(必须做)
- 声明组件:在代码中声明各种UI组件变量(重复工作)
- 查找组件:通过代码获取UI控件的引用(重复工作)
- 监听事件:为按钮、滑动条等添加事件监听(重复工作)
- 处理逻辑:实现UI的具体业务逻辑(必须做)
核心问题:
- 每次制作UI面板时,步骤2、3、4都需要重复编写大量冗余代码
- 缺乏统一的UI管理机制,面板的显示隐藏逻辑分散
- 面板之间缺乏层级管理,UI显示顺序混乱
解决方案:
通过创建UI管理模块,提供以下功能:
- UI面板基类(BasePanel):自动查找组件、监听事件,无需重复编写冗余代码
- UI管理器(UIMgr):统一管理所有UI面板的显示隐藏,支持层级管理
- 自动事件管理:通过命名规则自动为组件添加事件监听
- 资源管理整合:与资源管理模块整合,支持同步和异步加载面板
主要功能:
- 自动组件查找:通过命名规则自动查找并管理UI组件
- 自动事件监听:为Button、Slider、Toggle等组件自动添加事件监听
- 面板管理:统一管理面板的创建、显示、隐藏和销毁
- 层级管理:支持Bottom、Middle、Top、System四层UI管理
- 异步加载:支持面板异步加载,避免卡顿
- 可选销毁:支持失活和销毁两种模式,提高性能
UI模块的基本原理
设计理念:
- 自动化:通过命名规则和反射机制自动查找组件
- 统一管理:通过UIMgr统一管理所有面板的生命周期
- 层级分离:通过Canvas层级实现UI的前后关系管理
- 资源整合:整合资源管理模块,支持AB包加载
- 状态管理:使用PanelInfo管理面板的加载状态和回调
工作流程:
- 面板创建:通过UIMgr.ShowPanel创建面板,从AB包异步加载
- 组件查找:BasePanel在Awake中自动查找所有子组件
- 事件监听:根据组件类型自动添加相应的事件监听
- 面板显示:调用ShowMe方法,执行自定义逻辑
- 面板隐藏:调用HideMe方法,可选择失活或销毁
- 自动清理:失活的面板在再次显示时可复用
UI面板基类实现
为什么需要UI面板基类
传统的UI制作流程中,声明组件、查找组件、监听事件这三个步骤每次都重复编写,造成大量冗余代码。通过制作UI面板基类,我们可以:
- 自动查找组件:通过命名规则和反射自动获取UI组件引用
- 自动添加事件:根据组件类型自动添加相应的事件监听
- 统一样式:为所有面板提供统一的获取组件方法
UI面板基类实现思路
主要实现思路:
- 制定命名规则:需要使用的组件重命名,不使用的组件保持默认名
- 自动查找组件:在Awake中使用GetComponentsInChildren自动查找所有子组件
- 自动添加事件:检测组件类型(Button、Slider、Toggle),自动添加事件监听
- 提供获取方法:提供GetControl方法,让子类获取指定组件
- 虚方法设计:提供ShowMe和HideMe虚方法,供子类重写
设计要点:
- 默认名字过滤:准备一个默认名字容器,过滤掉用于显示的组件(如Image、Text的背景子对象)
- 组件优先级:优先查找重要组件(Button、Toggle等),避免同一对象上挂多个组件的冲突
- 里式替换原则:使用Dictionary存储所有组件,父类容器装子类对象
源码实现:BasePanel.cs
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// UI面板基类
/// 提供自动查找组件、自动监听事件的功能
/// </summary>
public abstract class BasePanel : MonoBehaviour
{
/// <summary>
/// 用于存储所有要用到的UI控件
/// 使用里式替换原则,父类容器装子类对象
/// </summary>
protected Dictionary<string, UIBehaviour> controlDic = new Dictionary<string, UIBehaviour>();
/// <summary>
/// 控件默认名字列表
/// 如果得到的控件名字存在于这个容器,意味着我们不会通过代码去使用它
/// 它只会起到显示作用的控件(如Image背景、Text占位符等)
/// </summary>
private static List<string> defaultNameList = new List<string>()
{
"Image",
"Text (TMP)",
"RawImage",
"Background",
"Checkmark",
"Label",
"Text (Legacy)",
"Arrow",
"Placeholder",
"Fill",
"Handle",
"Viewport",
"Scrollbar Horizontal",
"Scrollbar Vertical"
};
/// <summary>
/// Awake方法:自动查找组件
/// 为了避免某一个对象上存在多种控件的情况,应该优先查找重要的组件
/// </summary>
protected virtual void Awake()
{
// 优先查找重要组件(交互组件)
FindChildrenControl<Button>();
FindChildrenControl<Toggle>();
FindChildrenControl<Slider>();
FindChildrenControl<InputField>();
FindChildrenControl<ScrollRect>();
FindChildrenControl<Dropdown>();
// 即使对象上挂了多个组件,只要优先找到了重要组件
// 之后也可以通过重要组件得到身上其他挂载的内容
FindChildrenControl<Text>();
FindChildrenControl<TextMeshProUGUI>();
FindChildrenControl<Image>();
}
/// <summary>
/// 面板显示时会调用的逻辑
/// </summary>
public abstract void ShowMe();
/// <summary>
/// 面板隐藏时会调用的逻辑
/// </summary>
public abstract void HideMe();
/// <summary>
/// 获取指定名字以及指定类型的组件
/// </summary>
/// <typeparam name="T">组件类型</typeparam>
/// <param name="name">组件名字</param>
/// <returns>指定类型的组件</returns>
public T GetControl<T>(string name) where T : UIBehaviour
{
if (controlDic.ContainsKey(name))
{
T control = controlDic[name] as T;
if (control == null)
Debug.LogError($"不存在对应名字{name}类型为{typeof(T)}的组件");
return control;
}
else
{
Debug.LogError($"不存在对应名字{name}的组件");
return null;
}
}
/// <summary>
/// 按钮点击事件处理
/// 子类可以重写此方法来处理自定义逻辑 比如switch名字
/// </summary>
/// <param name="btnName">按钮名称</param>
protected virtual void ClickBtn(string btnName)
{
// 默认实现为空,子类可以重写
}
/// <summary>
/// 滑动条值变化事件处理
/// 子类可以重写此方法来处理自定义逻辑 比如switch名字
/// </summary>
/// <param name="sliderName">滑动条名称</param>
/// <param name="value">滑动条的值</param>
protected virtual void SliderValueChange(string sliderName, float value)
{
// 默认实现为空,子类可以重写
}
/// <summary>
/// 开关值变化事件处理
/// 子类可以重写此方法来处理自定义逻辑 比如switch名字
/// </summary>
/// <param name="toggleName">开关名称</param>
/// <param name="value">开关的值</param>
protected virtual void ToggleValueChange(string toggleName, bool value)
{
// 默认实现为空,子类可以重写
}
/// <summary>
/// 查找子对象中的指定类型组件
/// </summary>
/// <typeparam name="T">组件类型</typeparam>
private void FindChildrenControl<T>() where T : UIBehaviour
{
T[] controls = this.GetComponentsInChildren<T>(true);
for (int i = 0; i < controls.Length; i++)
{
// 获取当前控件的名字
string controlName = controls[i].gameObject.name;
// 将对应组件记录到字典中
if (!controlDic.ContainsKey(controlName))
{
// 过滤默认名字的组件
if (!defaultNameList.Contains(controlName))
{
controlDic.Add(controlName, controls[i]);
// 判断控件的类型,决定是否添加事件监听
if (controls[i] is Button)
{
(controls[i] as Button).onClick.AddListener(() =>
{
ClickBtn(controlName);
});
}
else if (controls[i] is Slider)
{
(controls[i] as Slider).onValueChanged.AddListener((value) =>
{
SliderValueChange(controlName, value);
});
}
else if (controls[i] is Toggle)
{
(controls[i] as Toggle).onValueChanged.AddListener((value) =>
{
ToggleValueChange(controlName, value);
});
}
}
}
}
}
}
UI面板基类测试
创建一个测试面板来验证BasePanel的功能。
测试面板:BeginPanel.cs
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 开始面板
/// 演示如何使用BasePanel基类
/// </summary>
public class BeginPanel : BasePanel
{
private void Start()
{
// 获取按钮组件
Button btnBegin = GetControl<Button>("btnBegin");
Debug.Log("获取到的按钮控件名为:" + btnBegin.name);
// 输出:获取到的按钮控件名为:btnBegin
}
/// <summary>
/// 重写按钮点击事件处理
/// </summary>
/// <param name="btnName">按钮名称</param>
protected override void ClickBtn(string btnName)
{
switch (btnName)
{
case "btnBegin":
Debug.Log("开始按钮被点击");
// 输出:开始按钮被点击
break;
case "btnSetting":
Debug.Log("设置按钮被点击");
// 输出:设置按钮被点击
break;
case "btnQuit":
Debug.Log("退出按钮被点击");
// 输出:退出按钮被点击
break;
}
}
public override void ShowMe()
{
Debug.Log("显示面板时BeginPanel会执行的默认逻辑");
}
public override void HideMe()
{
Debug.Log("隐藏面板时BeginPanel会执行的默认逻辑");
}
}
UI管理器实现
UI管理器的目标
UI管理器的目标:
- 统一管理:统一管理所有UI面板的显示隐藏
- 层级管理:通过Canvas层级实现UI的前后关系
- 自动创建:自动创建Canvas、EventSystem等UI基础设施
- 资源整合:与资源管理模块整合,支持异步加载面板
UI管理器层级规划
主要思路:
- Canvas和EventSystem:UI面板在任何场景都会显示,因此Canvas和EventSystem对象应该过场景不移除,并且保证唯一性和动态创建
- UI摄像机:使用独立的UI摄像机渲染UI(可选)
- 层级对象:在Canvas下创建管理层级的子对象,之后面板作为对应层级对象的子对象达到分层作用
层级设计:
- Bottom层:底层,通常放置背景、装饰等
- Middle层:中间层,放置主要UI内容
- Top层:顶层,放置提示、弹窗等
- System层:系统层,最高层,放置系统级UI(如Loading、断网提示等)
源码实现:UIMgr.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// UI层级枚举
/// 定义UI面板可以显示的层级
/// </summary>
public enum E_UILayer
{
/// <summary>
/// 最底层
/// </summary>
Bottom,
/// <summary>
/// 中层
/// </summary>
Middle,
/// <summary>
/// 高层
/// </summary>
Top,
/// <summary>
/// 系统层 最高层
/// </summary>
System,
}
/// <summary>
/// UI管理器
/// 管理所有UI面板的显示隐藏,提供层级管理
/// </summary>
public class UIMgr : Singleton<UIMgr>
{
#region 字段
/// <summary>
/// UI摄像机
/// </summary>
private Camera uiCamera;
/// <summary>
/// UI画布
/// </summary>
private Canvas uiCanvas;
/// <summary>
/// UI事件系统
/// </summary>
private EventSystem uiEventSystem;
/// <summary>
/// 层级父对象
/// </summary>
private Transform bottomLayer;
private Transform middleLayer;
private Transform topLayer;
private Transform systemLayer;
#endregion
#region 构造函数
private UIMgr()
{
// 动态创建唯一的Camera和EventSystem
uiCamera = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/UICamera")).GetComponent<Camera>();
// UI摄像机过场景不移除 专门用来渲染UI面板
GameObject.DontDestroyOnLoad(uiCamera.gameObject);
// 动态创建Canvas
uiCanvas = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/Canvas")).GetComponent<Canvas>();
// 设置使用的UI摄像机
uiCanvas.worldCamera = uiCamera;
// 过场景不移除
GameObject.DontDestroyOnLoad(uiCanvas.gameObject);
// 找到层级父对象
bottomLayer = uiCanvas.transform.Find("Bottom");
middleLayer = uiCanvas.transform.Find("Middle");
topLayer = uiCanvas.transform.Find("Top");
systemLayer = uiCanvas.transform.Find("System");
// 动态创建EventSystem
uiEventSystem = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/EventSystem")).GetComponent<EventSystem>();
GameObject.DontDestroyOnLoad(uiEventSystem.gameObject);
}
#endregion
#region 层级管理
/// <summary>
/// 获取对应层级的父对象
/// </summary>
/// <param name="layer">层级枚举值</param>
/// <returns>层级父对象</returns>
public Transform GetLayerFather(E_UILayer layer)
{
switch (layer)
{
case E_UILayer.Bottom:
return bottomLayer;
case E_UILayer.Middle:
return middleLayer;
case E_UILayer.Top:
return topLayer;
case E_UILayer.System:
return systemLayer;
default:
return null;
}
}
#endregion
}
UI管理器基础实现
主要实现内容:
- 存储面板的容器:使用Dictionary存储所有面板
- 显示面板:加载面板并显示到指定层级
- 隐藏面板:隐藏并销毁面板
- 获取面板:获取指定面板的引用
源码实现:UIMgr.cs
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 管理所有UI面板的管理器
/// 注意:面板预设体名要和面板类名一致!
/// </summary>
public class UIMgr : Singleton<UIMgr>
{
#region 字段
/// <summary>
/// 用于存储所有的面板对象
/// </summary>
private Dictionary<string, BasePanel> panelDic = new Dictionary<string, BasePanel>();
#endregion
#region 构造函数
private UIMgr()
{
// 构造函数内容与Layer33相同,省略...
}
#endregion
#region 层级管理
/// <summary>
/// 获取对应层级的父对象
/// </summary>
public Transform GetLayerFather(E_UILayer layer)
{
// 实现与Lesson33相同,省略...
}
#endregion
#region 面板管理
/// <summary>
/// 显示面板
/// </summary>
/// <typeparam name="T">面板的类型</typeparam>
/// <param name="layer">面板显示的层级,默认为Middle</param>
/// <param name="callBack">由于可能是异步加载,因此通过委托回调的形式将加载完成的面板传递出去</param>
/// <param name="isSync">是否采用同步加载,默认为false</param>
public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle, UnityAction<T> callBack = null, bool isSync = false) where T : BasePanel
{
// 获取面板名 预设体名必须和面板类名一致
string panelName = typeof(T).Name;
// 存在面板
if (panelDic.ContainsKey(panelName))
{
// 如果要显示面板,会执行一次面板的默认显示逻辑
panelDic[panelName].ShowMe();
// 如果存在回调,直接返回出去即可
callBack?.Invoke(panelDic[panelName] as T);
return;
}
// 不存在面板,加载面板
ABResMgr.Instance.LoadResAsync<GameObject>("ui", panelName, (res) =>
{
// 层级的处理
Transform father = GetLayerFather(layer);
// 避免没有按指定规则传递层级参数,避免为空
if (father == null)
father = middleLayer;
// 将面板预设体创建到对应父对象下,并且保持原本的缩放大小
GameObject panelObj = GameObject.Instantiate(res, father, false);
// 获取对应UI组件返回出去
T panel = panelObj.GetComponent<T>();
// 显示面板时执行的默认方法
panel.ShowMe();
// 传出去使用
callBack?.Invoke(panel);
// 存储panel
panelDic.Add(panelName, panel);
}, isSync);
}
/// <summary>
/// 隐藏面板
/// </summary>
/// <typeparam name="T">面板类型</typeparam>
public void HidePanel<T>() where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
{
// 执行默认的隐藏面板想要做的事情
panelDic[panelName].HideMe();
// 销毁面板
GameObject.Destroy(panelDic[panelName].gameObject);
// 从容器中移除
panelDic.Remove(panelName);
}
}
/// <summary>
/// 获取面板
/// </summary>
/// <typeparam name="T">面板的类型</typeparam>
/// <returns>面板对象</returns>
public T GetPanel<T>() where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
return panelDic[panelName] as T;
return null;
}
#endregion
}
测试代码:
using UnityEngine;
public class UIBasicTest : MonoBehaviour
{
void Start()
{
Debug.Log("UI基础功能测试:开始");
// 显示面板
UIMgr.Instance.ShowPanel<BeginPanel>(E_UILayer.Middle, (panel) =>
{
Debug.Log("面板显示完成:" + panel.name);
// 输出:面板显示完成:BeginPanel
});
// 延迟隐藏面板
Invoke("HidePanel", 3f);
}
private void HidePanel()
{
UIMgr.Instance.HidePanel<BeginPanel>();
Debug.Log("面板已隐藏");
}
}
异步加载优化
为什么需要异步加载优化
在使用异步加载时,可能会遇到以下问题:
- 同一帧连续显示同一面板:会触发多次异步加载,导致重复创建
- 同一帧显示又隐藏同一面板:异步加载结束后还会创建面板,造成浪费
- 获取面板时如果正在加载中:会返回null,无法获取面板引用
异步加载优化思路
主要制作思路:
- 造成问题的关键点:由于异步加载,字典容器中没有及时存储将要显示的面板对象
- 解决方案:我们需要在显示面板时,一开始就存储面板的相关信息,这样不管是二次显示还是隐藏,都能够知道是否已经在加载面板了
- 分情况考虑:
- 显示相关:若加载中想要显示,应该记录回调,加载结束后统一调用
- 隐藏相关:若加载中想要隐藏,应该改变标识,加载结束后不创建面板
- 获取相关:若加载中想要获取,应该等待加载结束后再处理获取逻辑
主要解决的问题:
- 同一帧连续显示同一面板,避免重复进行异步加载后回调重复往字典中添加面板数据
- 同一帧显示—>隐藏—>显示同一面板,面板能够正常显示
- 获取面板时如果正在加载中,等加载结束后再获取处理逻辑
源码实现:UIMgr.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 管理所有UI面板的管理器(异步加载优化版本)
/// 注意:面板预设体名要和面板类名一致!
/// </summary>
public class UIMgr : Singleton<UIMgr>
{
#region 内部类
/// <summary>
/// 主要用于里式替换原则,在字典中用父类容器装载子类对象
/// </summary>
private abstract class BasePanelInfo { }
/// <summary>
/// 用于存储面板信息和加载完成的回调函数
/// </summary>
/// <typeparam name="T">面板的类型</typeparam>
private class PanelInfo<T> : BasePanelInfo where T : BasePanel
{
/// <summary>
/// 面板对象
/// null表示正在加载中
/// </summary>
public T panel;
/// <summary>
/// 回调函数委托
/// </summary>
public UnityAction<T> callBack;
/// <summary>
/// 是否隐藏
/// true表示在加载完成前就被标记为隐藏
/// </summary>
public bool isHide;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="callBack">回调函数</param>
public PanelInfo(UnityAction<T> callBack)
{
this.callBack += callBack;
}
}
#endregion
#region 字段
/// <summary>
/// 用于存储所有的面板对象
/// </summary>
private Dictionary<string, BasePanelInfo> panelDic = new Dictionary<string, BasePanelInfo>();
// ... 其他字段省略(与Lesson33相同)
#endregion
#region 面板管理
/// <summary>
/// 显示面板
/// </summary>
public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle, UnityAction<T> callBack = null, bool isSync = false) where T : BasePanel
{
string panelName = typeof(T).Name;
// 存在面板(已加载或正在加载)
if (panelDic.ContainsKey(panelName))
{
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
// 正在异步加载中
if (panelInfo.panel == null)
{
// 如果之前显示了又隐藏,现在又想显示,那么直接设为false
panelInfo.isHide = false;
// 如果正在异步加载,应该等待它加载完毕,只需要记录回调函数,加载完后去调用即可
if (callBack != null)
panelInfo.callBack += callBack;
}
else// 已经加载结束
{
// 如果要显示面板,会执行一次面板的默认显示逻辑
panelInfo.panel.ShowMe();
// 如果存在回调,直接返回出去即可
callBack?.Invoke(panelInfo.panel);
}
return;
}
// 不存在面板,先存入字典当中占个位置,之后如果又显示,我才能得到字典中的信息进行判断
panelDic.Add(panelName, new PanelInfo<T>(callBack));
// 加载面板
ABResMgr.Instance.LoadResAsync<GameObject>("ui", panelName, (res) =>
{
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
// 表示异步加载结束前就想要隐藏该面板了
if (panelInfo.isHide)
{
panelDic.Remove(panelName);
return;
}
// 层级的处理
Transform father = GetLayerFather(layer);
if (father == null)
father = middleLayer;
// 将面板预设体创建到对应父对象下
GameObject panelObj = GameObject.Instantiate(res, father, false);
// 获取对应UI组件
T panel = panelObj.GetComponent<T>();
// 显示面板时执行的默认方法
panel.ShowMe();
// 传出去使用
panelInfo.callBack?.Invoke(panel);
// 回调执行完,将其清空,避免内存泄漏
panelInfo.callBack = null;
// 存储panel
panelInfo.panel = panel;
}, isSync);
}
/// <summary>
/// 隐藏面板
/// </summary>
/// <typeparam name="T">面板类型</typeparam>
public void HidePanel<T>() where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
{
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
// 正在加载中
if (panelInfo.panel == null)
{
// 修改隐藏标识,表示这个面板即将要隐藏
panelInfo.isHide = true;
// 既然要隐藏了,回调函数都不会调用了,直接置空
panelInfo.callBack = null;
}
else// 已经加载结束
{
// 执行默认的隐藏面板想要做的事情
panelInfo.panel.HideMe();
// 销毁面板
GameObject.Destroy(panelInfo.panel.gameObject);
// 从容器中移除
panelDic.Remove(panelName);
}
}
}
/// <summary>
/// 获取面板
/// </summary>
public void GetPanel<T>(UnityAction<T> callBack) where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
{
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
// 正在加载中
if (panelInfo.panel == null)
{
// 加载中,应该等待加载结束,再通过回调传递给外部去使用
panelInfo.callBack += callBack;
}
else if (!panelInfo.isHide)// 加载结束并且没有隐藏
{
callBack?.Invoke(panelInfo.panel);
}
}
}
#endregion
}
隐藏面板可选销毁优化
为什么需要可选销毁功能
目前的隐藏面板逻辑会直接将面板销毁,下次创建时再重新创建。
销毁模式的优缺点:
- 优点:当存在内存压力时,直接销毁面板后,当内存不足时会触发GC,不会因为存在没有使用的面板引用而造成内存崩溃
- 缺点:会产生内存垃圾加快GC的触发,频繁的销毁创建会增加性能消耗
解决方案:
我们不能直接将面板隐藏改成不销毁,而应该改为可以让我们自己控制最好。我们可以根据项目的实际情况,选择性的使用失活或销毁。
可选销毁实现思路
主要制作思路:
- 无需使用缓存池,因为缓存池主要是提供给非唯一对象使用的
- UI面板大部分情况下是唯一的,因此我们直接在UI管理器中修改逻辑即可
- 实现内容:
- 隐藏面板时,可以选择销毁还是失活
- 显示面板时,如果存在直接激活,如果不存在再重新创建
源码实现:UIMgr.cs
/// <summary>
/// 隐藏面板(支持可选销毁)
/// </summary>
/// <typeparam name="T">面板类型</typeparam>
/// <param name="isDestory">是否销毁面板,false表示失活(可以复用)</param>
public void HidePanel<T>(bool isDestory = false) where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
{
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
// 正在加载中
if (panelInfo.panel == null)
{
// 修改隐藏标识
panelInfo.isHide = true;
// 置空回调
panelInfo.callBack = null;
}
else// 已经加载结束
{
// 执行默认的隐藏面板想要做的事情
panelInfo.panel.HideMe();
// 如果要销毁,就直接将面板销毁从字典中移除记录
if (isDestory)
{
// 销毁面板
GameObject.Destroy(panelInfo.panel.gameObject);
// 从容器中移除
panelDic.Remove(panelName);
}
// 如果不销毁,那么就只是失活,下次再显示的时候,直接复用即可
else
panelInfo.panel.gameObject.SetActive(false);
}
}
}
/// <summary>
/// 显示面板(更新以支持复用)
/// </summary>
public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle, UnityAction<T> callBack = null, bool isSync = false) where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
{
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
if (panelInfo.panel == null)
{
// 加载中的情况...
panelInfo.isHide = false;
if (callBack != null)
panelInfo.callBack += callBack;
}
else// 已经加载结束
{
// 【新增】如果是失活状态,直接激活面板就可以显示了
if (!panelInfo.panel.gameObject.activeSelf)
panelInfo.panel.gameObject.SetActive(true);
// 如果要显示面板,会执行一次面板的默认显示逻辑
panelInfo.panel.ShowMe();
callBack?.Invoke(panelInfo.panel);
}
return;
}
// 加载面板的逻辑...
// (与Lesson35相同)
}
测试代码:
using UnityEngine;
public class UIOptionalDestroyTest : MonoBehaviour
{
void Start()
{
Debug.Log("UI可选销毁测试:开始");
// 显示面板
UIMgr.Instance.ShowPanel<BeginPanel>((panel) =>
{
Debug.Log("面板显示完成");
});
// 延迟3秒后失活面板(不销毁)
Invoke("DeactivatePanel", 3f);
// 延迟5秒后再次显示(应该复用)
Invoke("ReactivatePanel", 5f);
// 延迟8秒后销毁面板
Invoke("DestroyPanel", 8f);
}
private void DeactivatePanel()
{
UIMgr.Instance.HidePanel<BeginPanel>(false); // 失活
Debug.Log("面板已失活(可复用)");
}
private void ReactivatePanel()
{
UIMgr.Instance.ShowPanel<BeginPanel>((panel) =>
{
Debug.Log("面板复用成功(无需重新加载)");
});
}
private void DestroyPanel()
{
UIMgr.Instance.HidePanel<BeginPanel>(true); // 销毁
Debug.Log("面板已销毁");
}
}
自定义事件添加功能
为什么需要自定义事件
在制作UI功能时,经常会有这样的需求:
- 为一些不带默认事件的控件添加自定义事件,比如Image、Text这些基础组件,想为他们添加点击、拖拽等事件监听
- 为一些带默认事件的控件添加自定义事件,比如为Button按钮添加鼠标进入、鼠标移除等事件监听
因此我们完全可以把添加自定义事件封装为一个公共函数,方便为各种控件添加自定义事件。
自定义事件实现思路
主要实现思路:
- 为想要添加自定义事件的控件添加EventTrigger组件
- 通过EventTrigger组件添加对应自定义事件的监听
源码实现:UIMgr.cs
/// <summary>
/// 为控件添加自定义事件
/// </summary>
/// <param name="control">对应的控件</param>
/// <param name="type">事件的类型(EventTriggerType)</param>
/// <param name="callBack">响应的函数</param>
public static void AddCustomEventListener(UIBehaviour control, EventTriggerType type, UnityAction<BaseEventData> callBack)
{
// 这种逻辑主要是用于保证控件上只会挂载一个EventTrigger
EventTrigger trigger = control.GetComponent<EventTrigger>();
if (trigger == null)
trigger = control.gameObject.AddComponent<EventTrigger>();
// 创建事件触发器条目
EventTrigger.Entry entry = new EventTrigger.Entry();
entry.eventID = type;
entry.callback.AddListener(callBack);
// 添加到触发器的触发器列表中
trigger.triggers.Add(entry);
}
测试代码:
using UnityEngine;
using UnityEngine.EventSystems;
public class BeginPanel : BasePanel
{
private void Start()
{
// 获取按钮
Button btnBegin = GetControl<Button>("btnBegin");
// 为按钮添加自定义事件:鼠标进入
UIMgr.AddCustomEventListener(btnBegin, EventTriggerType.PointerEnter, (data) =>
{
Debug.Log("鼠标进入按钮");
});
// 为按钮添加自定义事件:鼠标离开
UIMgr.AddCustomEventListener(btnBegin, EventTriggerType.PointerExit, (data) =>
{
Debug.Log("鼠标离开按钮");
});
// 为按钮添加自定义事件:拖拽
UIMgr.AddCustomEventListener(btnBegin, EventTriggerType.Drag, (data) =>
{
PointerEventData eventData = data as PointerEventData;
Debug.Log("按钮被拖拽:" + eventData.position);
});
}
protected override void ClickBtn(string btnName)
{
switch (btnName)
{
case "btnBegin":
Debug.Log("开始按钮被点击");
break;
}
}
public override void ShowMe()
{
Debug.Log("BeginPanel显示");
}
public override void HideMe()
{
Debug.Log("BeginPanel隐藏");
}
}
测试和使用
创建一个完整的测试场景,展示UI模块的所有功能。
using UnityEngine;
public class UICompleteTest : MonoBehaviour
{
void Start()
{
Debug.Log("完整UI系统测试:开始");
// 1. 测试显示面板
TestShowPanel();
// 2. 测试异步加载
TestAsyncLoad();
// 3. 测试失活和复用
TestDeactivateAndReuse();
// 4. 测试获取面板
TestGetPanel();
}
/// <summary>
/// 测试显示面板
/// </summary>
private void TestShowPanel()
{
Debug.Log("测试显示面板");
// 在Middle层显示BeginPanel
UIMgr.Instance.ShowPanel<BeginPanel>(E_UILayer.Middle, (panel) =>
{
Debug.Log("BeginPanel显示完成");
// 输出:BeginPanel显示完成
});
// 延迟2秒显示顶层面板
Invoke("ShowTopPanel", 2f);
}
private void ShowTopPanel()
{
// 在Top层显示TipPanel(假设存在)
UIMgr.Instance.ShowPanel<TipPanel>(E_UILayer.Top, (panel) =>
{
Debug.Log("TipPanel显示完成");
// 输出:TipPanel显示完成(会覆盖在BeginPanel上方)
});
}
/// <summary>
/// 测试异步加载
/// </summary>
private void TestAsyncLoad()
{
Debug.Log("测试异步加载");
// 同一帧连续调用两次ShowPanel
UIMgr.Instance.ShowPanel<TestPanel>((panel1) =>
{
Debug.Log("第一次回调:" + panel1.name);
});
// 第二次调用(应该复用)
UIMgr.Instance.ShowPanel<TestPanel>((panel2) =>
{
Debug.Log("第二次回调:" + panel2.name);
// 输出:两次应该都返回同一个面板实例
});
}
/// <summary>
/// 测试失活和复用
/// </summary>
private void TestDeactivateAndReuse()
{
Debug.Log("测试失活和复用");
// 延迟5秒失活面板
Invoke("DeactivatePanel", 5f);
// 延迟7秒再次显示(应该复用)
Invoke("ReusePanel", 7f);
}
private void DeactivatePanel()
{
UIMgr.Instance.HidePanel<BeginPanel>(false); // 失活
Debug.Log("面板已失活");
}
private void ReusePanel()
{
UIMgr.Instance.ShowPanel<BeginPanel>((panel) =>
{
Debug.Log("面板已复用(无需重新创建)");
});
}
/// <summary>
/// 测试获取面板
/// </summary>
private void TestGetPanel()
{
Debug.Log("测试获取面板");
UIMgr.Instance.GetPanel<BeginPanel>((panel) =>
{
if (panel != null)
{
Debug.Log("成功获取面板:" + panel.name);
}
else
{
Debug.Log("面板不存在或正在加载中");
}
});
}
}
进阶和拓展
可以结合MVC框架以及编辑器拓展中自动生成UI代码来完善UI模块。
8.2 知识点代码
UIMgr.cs(UI管理器 - 最终版本)
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
/// <summary>
/// UI层级枚举
/// 定义UI面板可以显示的层级
/// </summary>
public enum E_UILayer
{
/// <summary>
/// 最底层
/// </summary>
Bottom,
/// <summary>
/// 中层
/// </summary>
Middle,
/// <summary>
/// 高层
/// </summary>
Top,
/// <summary>
/// 系统层 最高层
/// </summary>
System,
}
/// <summary>
/// 管理所有UI面板的管理器
/// 提供面板的显示隐藏、层级管理、异步加载等功能
/// 注意:面板预设体名要和面板类名一致!
/// </summary>
public class UIMgr : Singleton<UIMgr>
{
#region 内部类
/// <summary>
/// 主要用于里式替换原则,在字典中用父类容器装载子类对象
/// </summary>
private abstract class BasePanelInfo { }
/// <summary>
/// 用于存储面板信息和加载完成的回调函数
/// </summary>
/// <typeparam name="T">面板的类型</typeparam>
private class PanelInfo<T> : BasePanelInfo where T : BasePanel
{
/// <summary>
/// 面板对象
/// null表示正在加载中
/// </summary>
public T panel;
/// <summary>
/// 回调函数委托
/// </summary>
public UnityAction<T> callBack;
/// <summary>
/// 是否隐藏
/// true表示在加载完成前就被标记为隐藏
/// </summary>
public bool isHide;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="callBack">回调函数</param>
public PanelInfo(UnityAction<T> callBack)
{
this.callBack += callBack;
}
}
#endregion
#region 字段
/// <summary>
/// UI摄像机
/// </summary>
private Camera uiCamera;
/// <summary>
/// UI画布
/// </summary>
private Canvas uiCanvas;
/// <summary>
/// UI事件系统
/// </summary>
private EventSystem uiEventSystem;
/// <summary>
/// 层级父对象
/// </summary>
private Transform bottomLayer;
private Transform middleLayer;
private Transform topLayer;
private Transform systemLayer;
/// <summary>
/// 用于存储所有的面板对象
/// </summary>
private Dictionary<string, BasePanelInfo> panelDic = new Dictionary<string, BasePanelInfo>();
#endregion
#region 构造函数
private UIMgr()
{
// 动态创建唯一的Camera和EventSystem
uiCamera = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/UICamera")).GetComponent<Camera>();
// UI摄像机过场景不移除,专门用来渲染UI面板
GameObject.DontDestroyOnLoad(uiCamera.gameObject);
// 动态创建Canvas
uiCanvas = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/Canvas")).GetComponent<Canvas>();
// 设置使用的UI摄像机
uiCanvas.worldCamera = uiCamera;
// 过场景不移除
GameObject.DontDestroyOnLoad(uiCanvas.gameObject);
// 找到层级父对象
bottomLayer = uiCanvas.transform.Find("Bottom");
middleLayer = uiCanvas.transform.Find("Middle");
topLayer = uiCanvas.transform.Find("Top");
systemLayer = uiCanvas.transform.Find("System");
// 动态创建EventSystem
uiEventSystem = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/EventSystem")).GetComponent<EventSystem>();
GameObject.DontDestroyOnLoad(uiEventSystem.gameObject);
}
#endregion
#region 层级管理
/// <summary>
/// 获取对应层级的父对象
/// </summary>
/// <param name="layer">层级枚举值</param>
/// <returns>层级父对象</returns>
public Transform GetLayerFather(E_UILayer layer)
{
switch (layer)
{
case E_UILayer.Bottom:
return bottomLayer;
case E_UILayer.Middle:
return middleLayer;
case E_UILayer.Top:
return topLayer;
case E_UILayer.System:
return systemLayer;
default:
return null;
}
}
#endregion
#region 面板管理
/// <summary>
/// 显示面板
/// </summary>
/// <typeparam name="T">面板的类型</typeparam>
/// <param name="layer">面板显示的层级,默认为Middle</param>
/// <param name="callBack">由于可能是异步加载,因此通过委托回调的形式将加载完成的面板传递出去</param>
/// <param name="isSync">是否采用同步加载,默认为false</param>
public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle, UnityAction<T> callBack = null, bool isSync = false) where T : BasePanel
{
// 获取面板名,预设体名必须和面板类名一致
string panelName = typeof(T).Name;
// 存在面板(已加载或正在加载)
if (panelDic.ContainsKey(panelName))
{
// 取出字典中已经占好位置的数据
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
// 正在异步加载中
if (panelInfo.panel == null)
{
// 如果之前显示了又隐藏,现在又想显示,那么直接设为false
panelInfo.isHide = false;
// 如果正在异步加载,应该等待它加载完毕,只需要记录回调函数,加载完后去调用即可
if (callBack != null)
panelInfo.callBack += callBack;
}
else// 已经加载结束
{
// 如果是失活状态,直接激活面板就可以显示了
if (!panelInfo.panel.gameObject.activeSelf)
{
panelInfo.panel.gameObject.SetActive(true);
}
// 如果要显示面板,会执行一次面板的默认显示逻辑
panelInfo.panel.ShowMe();
// 如果存在回调,直接返回出去即可
callBack?.Invoke(panelInfo.panel);
}
return;
}
// 不存在面板,先存入字典当中占个位置
panelDic.Add(panelName, new PanelInfo<T>(callBack));
// 不存在面板,加载面板
ABResMgr.Instance.LoadResAsync<GameObject>("ui", panelName, (res) =>
{
// 取出字典中已经占好位置的数据
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
// 表示异步加载结束前就想要隐藏该面板了
if (panelInfo.isHide)
{
panelDic.Remove(panelName);
return;
}
// 层级的处理
Transform father = GetLayerFather(layer);
// 避免没有按指定规则传递层级参数,避免为空
if (father == null)
father = middleLayer;
// 将面板预设体创建到对应父对象下,并且保持原本的缩放大小
GameObject panelObj = GameObject.Instantiate(res, father, false);
// 获取对应UI组件
T panel = panelObj.GetComponent<T>();
// 显示面板时执行的默认方法
panel.ShowMe();
// 传出去使用
panelInfo.callBack?.Invoke(panel);
// 回调执行完,将其清空,避免内存泄漏
panelInfo.callBack = null;
// 存储panel
panelInfo.panel = panel;
}, isSync);
}
/// <summary>
/// 隐藏面板
/// </summary>
/// <typeparam name="T">面板类型</typeparam>
/// <param name="isDestory">是否销毁面板,false表示失活(可以复用)</param>
public void HidePanel<T>(bool isDestory = false) where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
{
// 取出字典中已经占好位置的数据
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
// 但是正在加载中
if (panelInfo.panel == null)
{
// 修改隐藏标识,表示这个面板即将要隐藏
panelInfo.isHide = true;
// 既然要隐藏了,回调函数都不会调用了,直接置空
panelInfo.callBack = null;
}
else// 已经加载结束
{
// 执行默认的隐藏面板想要做的事情
panelInfo.panel.HideMe();
// 如果要销毁,就直接将面板销毁从字典中移除记录
if (isDestory)
{
// 销毁面板
GameObject.Destroy(panelInfo.panel.gameObject);
// 从容器中移除
panelDic.Remove(panelName);
}
// 如果不销毁,那么就只是失活,下次再显示的时候,直接复用即可
else
panelInfo.panel.gameObject.SetActive(false);
}
}
}
/// <summary>
/// 获取面板
/// </summary>
/// <typeparam name="T">面板的类型</typeparam>
/// <param name="callBack">获取到面板后的回调函数</param>
public void GetPanel<T>(UnityAction<T> callBack) where T : BasePanel
{
string panelName = typeof(T).Name;
if (panelDic.ContainsKey(panelName))
{
// 取出字典中已经占好位置的数据
PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
// 正在加载中
if (panelInfo.panel == null)
{
// 加载中,应该等待加载结束,再通过回调传递给外部去使用
panelInfo.callBack += callBack;
}
else if (!panelInfo.isHide)// 加载结束并且没有隐藏
{
callBack?.Invoke(panelInfo.panel);
}
}
}
#endregion
#region 自定义事件
/// <summary>
/// 为控件添加自定义事件
/// </summary>
/// <param name="control">对应的控件</param>
/// <param name="type">事件的类型</param>
/// <param name="callBack">响应的函数</param>
public static void AddCustomEventListener(UIBehaviour control, EventTriggerType type, UnityAction<BaseEventData> callBack)
{
// 这种逻辑主要是用于保证控件上只会挂载一个EventTrigger
EventTrigger trigger = control.GetComponent<EventTrigger>();
if (trigger == null)
{
trigger = control.gameObject.AddComponent<EventTrigger>();
}
EventTrigger.Entry entry = new EventTrigger.Entry();
entry.eventID = type;
entry.callback.AddListener(callBack);
trigger.triggers.Add(entry);
}
#endregion
}
BasePanel.cs(UI面板基类)
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// UI面板基类
/// 提供自动查找组件、自动监听事件的功能
/// </summary>
public abstract class BasePanel : MonoBehaviour
{
/// <summary>
/// 用于存储所有要用到的UI控件
/// 使用里式替换原则,父类容器装子类对象
/// </summary>
protected Dictionary<string, UIBehaviour> controlDic = new Dictionary<string, UIBehaviour>();
/// <summary>
/// 控件默认名字列表
/// 如果得到的控件名字存在于这个容器,意味着我们不会通过代码去使用它
/// 它只会起到显示作用的控件(如Image背景、Text占位符等)
/// </summary>
private static List<string> defaultNameList = new List<string>()
{
"Image",
"Text (TMP)",
"RawImage",
"Background",
"Checkmark",
"Label",
"Text (Legacy)",
"Arrow",
"Placeholder",
"Fill",
"Handle",
"Viewport",
"Scrollbar Horizontal",
"Scrollbar Vertical"
};
/// <summary>
/// Awake方法:自动查找组件
/// 为了避免某一个对象上存在多种控件的情况,应该优先查找重要的组件
/// </summary>
protected virtual void Awake()
{
// 优先查找重要组件(交互组件)
FindChildrenControl<Button>();
FindChildrenControl<Toggle>();
FindChildrenControl<Slider>();
FindChildrenControl<InputField>();
FindChildrenControl<ScrollRect>();
FindChildrenControl<Dropdown>();
// 即使对象上挂了多个组件,只要优先找到了重要组件
// 之后也可以通过重要组件得到身上其他挂载的内容
FindChildrenControl<Text>();
FindChildrenControl<TextMeshProUGUI>();
FindChildrenControl<Image>();
}
/// <summary>
/// 面板显示时会调用的逻辑
/// </summary>
public abstract void ShowMe();
/// <summary>
/// 面板隐藏时会调用的逻辑
/// </summary>
public abstract void HideMe();
/// <summary>
/// 获取指定名字以及指定类型的组件
/// </summary>
/// <typeparam name="T">组件类型</typeparam>
/// <param name="name">组件名字</param>
/// <returns>指定类型的组件</returns>
public T GetControl<T>(string name) where T : UIBehaviour
{
if (controlDic.ContainsKey(name))
{
T control = controlDic[name] as T;
if (control == null)
Debug.LogError($"不存在对应名字{name}类型为{typeof(T)}的组件");
return control;
}
else
{
Debug.LogError($"不存在对应名字{name}的组件");
return null;
}
}
/// <summary>
/// 按钮点击事件处理
/// 子类可以重写此方法来处理自定义逻辑
/// </summary>
/// <param name="btnName">按钮名称</param>
protected virtual void ClickBtn(string btnName)
{
// 默认实现为空,子类可以重写
}
/// <summary>
/// 滑动条值变化事件处理
/// 子类可以重写此方法来处理自定义逻辑
/// </summary>
/// <param name="sliderName">滑动条名称</param>
/// <param name="value">滑动条的值</param>
protected virtual void SliderValueChange(string sliderName, float value)
{
// 默认实现为空,子类可以重写
}
/// <summary>
/// 开关值变化事件处理
/// 子类可以重写此方法来处理自定义逻辑
/// </summary>
/// <param name="toggleName">开关名称</param>
/// <param name="value">开关的值</param>
protected virtual void ToggleValueChange(string toggleName, bool value)
{
// 默认实现为空,子类可以重写
}
/// <summary>
/// 查找子对象中的指定类型组件
/// </summary>
/// <typeparam name="T">组件类型</typeparam>
private void FindChildrenControl<T>() where T : UIBehaviour
{
T[] controls = this.GetComponentsInChildren<T>(true);
for (int i = 0; i < controls.Length; i++)
{
// 获取当前控件的名字
string controlName = controls[i].gameObject.name;
// 将对应组件记录到字典中
if (!controlDic.ContainsKey(controlName))
{
// 过滤默认名字的组件
if (!defaultNameList.Contains(controlName))
{
controlDic.Add(controlName, controls[i]);
// 判断控件的类型,决定是否添加事件监听
if (controls[i] is Button)
{
(controls[i] as Button).onClick.AddListener(() =>
{
ClickBtn(controlName);
});
}
else if (controls[i] is Slider)
{
(controls[i] as Slider).onValueChanged.AddListener((value) =>
{
SliderValueChange(controlName, value);
});
}
else if (controls[i] is Toggle)
{
(controls[i] as Toggle).onValueChanged.AddListener((value) =>
{
ToggleValueChange(controlName, value);
});
}
}
}
}
}
}
BeginPanel.cs(测试面板示例)
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
/// <summary>
/// 开始面板
/// 演示如何使用BasePanel基类
/// </summary>
public class BeginPanel : BasePanel
{
private void Start()
{
// 获取按钮组件
Button btnBegin = GetControl<Button>("btnBegin");
Debug.Log("获取到的按钮控件名为:" + btnBegin.name);
// 输出:获取到的按钮控件名为:btnBegin
// 为按钮添加自定义事件:鼠标进入
UIMgr.AddCustomEventListener(btnBegin, EventTriggerType.PointerEnter, (data) =>
{
Debug.Log("鼠标进入按钮");
});
// 为按钮添加自定义事件:鼠标离开
UIMgr.AddCustomEventListener(btnBegin, EventTriggerType.PointerExit, (data) =>
{
Debug.Log("鼠标离开按钮");
});
// 监听事件中心的事件
EventCenter.Instance.AddEventListener<float>(E_EventType.E_SceneLoadChange, ChangeScene);
}
private void ChangeScene(float progress)
{
Debug.Log("切换场景的进度:" + progress);
}
/// <summary>
/// 重写按钮点击事件处理
/// </summary>
/// <param name="btnName">按钮名称</param>
protected override void ClickBtn(string btnName)
{
switch (btnName)
{
case "btnBegin":
Debug.Log("开始按钮被点击");
// 可以在这里切换场景、打开其他面板等
break;
case "btnSetting":
Debug.Log("设置按钮被点击");
break;
case "btnQuit":
Debug.Log("退出按钮被点击");
break;
}
}
/// <summary>
/// 显示面板时的逻辑
/// </summary>
public override void ShowMe()
{
Debug.Log("BeginPanel显示");
}
/// <summary>
/// 隐藏面板时的逻辑
/// </summary>
public override void HideMe()
{
Debug.Log("BeginPanel隐藏");
}
private void OnDestroy()
{
// 移除事件监听
EventCenter.Instance.RemoveEventListener<float>(E_EventType.E_SceneLoadChange, ChangeScene);
}
}
UIModuleTest.cs(UI模块完整测试)
using UnityEngine;
/// <summary>
/// UI模块完整功能测试
/// 演示UI模块的所有功能
/// </summary>
public class UIModuleTest : MonoBehaviour
{
void Start()
{
Debug.Log("UI模块完整功能测试:开始");
// 1. 测试基础显示隐藏功能
TestBasicPanel();
// 2. 测试层级管理
TestLayerManagement();
// 3. 测试异步加载和状态管理
TestAsyncLoad();
// 4. 测试失活和复用
TestDeactivateAndReuse();
// 5. 测试自定义事件
TestCustomEvent();
}
/// <summary>
/// 测试基础面板显示隐藏
/// </summary>
private void TestBasicPanel()
{
Debug.Log("测试基础面板显示隐藏");
// 显示面板
UIMgr.Instance.ShowPanel<BeginPanel>(E_UILayer.Middle, (panel) =>
{
Debug.Log("面板显示完成:" + panel.name);
// 输出:面板显示完成:BeginPanel
});
// 延迟3秒隐藏
Invoke("HidePanel", 3f);
}
private void HidePanel()
{
UIMgr.Instance.HidePanel<BeginPanel>(true); // 销毁
Debug.Log("面板已销毁");
}
/// <summary>
/// 测试层级管理
/// </summary>
private void TestLayerManagement()
{
Debug.Log("测试层级管理");
// 在Bottom层显示背景面板
UIMgr.Instance.ShowPanel<BackgroundPanel>(E_UILayer.Bottom, (panel) =>
{
Debug.Log("背景面板显示完成");
});
// 在Middle层显示主面板
UIMgr.Instance.ShowPanel<BeginPanel>(E_UILayer.Middle, (panel) =>
{
Debug.Log("主面板显示完成");
});
// 延迟1秒在Top层显示提示
Invoke("ShowTipPanel", 1f);
}
private void ShowTipPanel()
{
UIMgr.Instance.ShowPanel<TipPanel>(E_UILayer.Top, (panel) =>
{
Debug.Log("提示面板显示完成(会覆盖在Middle层上方)");
});
}
/// <summary>
/// 测试异步加载和状态管理
/// </summary>
private void TestAsyncLoad()
{
Debug.Log("测试异步加载和状态管理");
// 同一帧连续调用两次ShowPanel
UIMgr.Instance.ShowPanel<TestPanel>((panel1) =>
{
Debug.Log("第一次回调:" + panel1.name);
});
// 第二次调用(应该复用,不会重复加载)
UIMgr.Instance.ShowPanel<TestPanel>((panel2) =>
{
Debug.Log("第二次回调:" + panel2.name);
// 两次应该都返回同一个面板实例
});
}
/// <summary>
/// 测试失活和复用
/// </summary>
private void TestDeactivateAndReuse()
{
Debug.Log("测试失活和复用");
// 延迟5秒失活面板
Invoke("DeactivatePanel", 5f);
// 延迟7秒复用面板
Invoke("ReusePanel", 7f);
}
private void DeactivatePanel()
{
UIMgr.Instance.HidePanel<BeginPanel>(false); // 失活
Debug.Log("面板已失活(未销毁,可复用)");
}
private void ReusePanel()
{
UIMgr.Instance.ShowPanel<BeginPanel>((panel) =>
{
Debug.Log("面板已复用(无需重新创建,节省性能)");
});
}
/// <summary>
/// 测试自定义事件
/// </summary>
private void TestCustomEvent()
{
Debug.Log("测试自定义事件");
UIMgr.Instance.ShowPanel<BeginPanel>((panel) =>
{
// 通过获取到的面板添加自定义事件
Button btnBegin = panel.GetControl<Button>("btnBegin");
// 添加拖拽事件
UIMgr.AddCustomEventListener(btnBegin, EventTriggerType.Drag, (data) =>
{
PointerEventData eventData = data as PointerEventData;
Debug.Log("按钮被拖拽,位置:" + eventData.position);
});
Debug.Log("自定义事件添加成功");
});
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com