用UIManager全局管理你的UI
回顾之前的面板基类
首先我们先回顾一下之前我们是怎么写UGUI的UI脚本的。
我们会先写一个面板基类,会定义一些公共方法,其他所有的面板脚本都会继承它,代码如下:
using System.Collections; |
以防遗忘,这里回顾一下此处用到的C#知识点:
1. 泛型与泛型约束 (Generics & Constraints)
- 知识点:
BasePanel中的<T>是泛型。它允许你编写一个类或方法,推迟指定具体的数据类型,直到你实际使用它时。 - 知识点:
where T : class是泛型约束。它告诉编译器:“这个T不能是随便什么类型(比如不能是int或float这种值类型),它必须是一个引用类型(类)。” - 作用: 这使得基类具有极强的通用性。比如你创建一个
LoginPanel : BasePanel,此时基类里的T就自动变成了LoginPanel。
2. 抽象类与抽象方法 (Abstract Class & Methods)
- 知识点:
public abstract class BasePanel中的abstract关键字定义了一个抽象类。抽象类就像是“半成品图纸”,它不能被直接实例化(你不能new BasePanel()或者把它直接挂在物体上)。 - 知识点:
public abstract void Init();是抽象方法。它只有声明没有方法体{}。 - 作用: 这是一种“强制契约”。任何继承
BasePanel的子类面板(比如背包面板、设置面板),都必须实现Init()方法,确保每个面板都有自己的初始化逻辑。
3. 虚方法 (Virtual Methods)
- 知识点:
protected virtual void Awake()和public virtual void ShowMe()中的virtual关键字定义了虚方法。 - 区别于抽象方法: 虚方法有具体的实现逻辑(比如
ShowMe里写了打印日志和激活物体)。 - 作用: 它允许子类根据需要进行重写(Override)。如果子类在显示时除了激活物体还需要播放一个动画,就可以重写
ShowMe方法;如果不需要特殊处理,直接沿用父类的逻辑即可。
4. 静态成员与单例模式 (Static Members & Singleton Pattern)
知识点:
private static T instance;定义了一个静态变量。静态变量不属于某个具体的对象实例,而是属于类本身。无论你创建了多少个对象,静态变量在内存中永远只有一份。设计模式应用: 这几行代码实现了一个简易的单例模式(Singleton):
private static T instance;
public static T Instance => instance;通过把
instance变成静态的,外部脚本就可以直接通过具体面板类名.Instance来全局访问这个面板(例如LoginPanel.Instance.ShowMe()),免去了频繁使用GameObject.Find查找物体的性能消耗。
5. C# 语法糖:表达式主体定义 (Expression-bodied Members)
知识点:
public static T Instance => instance;中使用了=>操作符。作用: 这是 C# 6.0 引入的语法糖,用于简化只读属性的写法。它等价于传统的:
public static T Instance
{
get { return instance; }
}
6. 安全的类型转换 (Safe Type Casting)
- 知识点:
instance = this as T;中的as关键字。 - 作用: 它是 C# 中用于引用类型转换的安全方式。如果是强转
(T)this,一旦转换失败会直接抛出异常导致程序崩溃;而使用as,如果this不能转换为T,只会默默返回null,程序更为安全。
继承之前面板基类的具体面板脚本
using System.Collections; |
在这个面板脚本中,我们重写了方法,在继承了父类功能的基础上添加了自己的逻辑, 例如Awake 和 ShowMe 里面的 base.Awake() 和 base.ShowMe()。base 代表父类。这表示:“我要先执行父类原有的逻辑(比如初始化单例、激活物体),然后再执行我自己的独有逻辑(获取 CanvasGroup 设置透明度)。”这是面向对象编程中复用代码的经典操作。
而今天,我们换一种方法来写我们的UI脚本。在前面的方法中,我们采用的是多单例模式,每个子面板虽然都继承了面板基类,但是都是独立的单例。如果我想关闭A面板,打开B面板,那么我还要再A面板中调用面板B。如果我想一下关闭所有面板,则根本做不到。现在,我们将另外设置一个UIManager脚本来统一管理所有的脚本。这样做的好处有:
1.能保证解耦性,一个面板不需要再认识另外一个面板,而只需要调用UIManager就可以操控另一个面板。
2.统一的资源调度,通过使用字典记载所有面板的打开关闭情况。
新的面板基类
下面是面板基类和增加的重点部分分析:
using System.Collections; |
1.加入了淡入淡出效果
我们在这个面板基类中加入了淡入淡出效果,这意味着以后所有继承这个基类的面板,天生就自带了平滑的淡入淡出效果,极大节约了代码量。
2. 委托与回调函数 (Delegates & Callbacks)
- 知识点:
private UnityAction hideCallBack = null;和public virtual void HideMe(UnityAction callback)。 - 概念说明:
UnityAction是 Unity 封装好的一种委托(Delegate),本质上它就是一个“可以存储方法的变量”。 - 作用: 这里的
callback就是回调函数。你可以把它想象成“留电话号码”。当你调用HideMe时,你不仅告诉面板“隐藏自己”,还塞给它一张纸条(callback):“等你完全变透明后,记得照着纸条上的指示做下一步(比如打开另一个面板)。”
3. 安全的事件执行防重入机制 (Safe Event Invocation)
UnityAction tempAction = hideCallBack; |
- 知识点:防循环调用(防重入)。如果直接写
hideCallBack?.Invoke();,假设在这个回调函数里,别人又调用了当前面板的方法或者触发了其他修改hideCallBack的逻辑,很容易产生死循环或数据错乱。 - 逻辑拆解:
- 先用一个临时变量
tempAction把委托存起来。 - 立刻把原有的
hideCallBack清空(切断引用)。 - 最后再执行临时变量里的逻辑。这样即使回调里又发生了什么奇葩操作,也不会影响当前面板的状态了。
- 先用一个临时变量
- 语法糖
?.(Null 条件运算符):tempAction?.Invoke()的意思是,如果tempAction不是空的,就执行它;如果是空的,就什么也不做。这比写if (tempAction != null) { tempAction(); }优雅得多。
增加的UIManager
using System.Collections; |
这段代码里涉及了非常多架构级别的 C# 和 Unity 知识点,我们来逐一拆解:
1. 纯 C# 单例模式 (饿汉式单例)
- 知识点:
private static UIManager instance = new UIManager();和public static UIManager Instance => instance; - 不同之处: 注意到这个类没有继承
MonoBehaviour吗?这意味着它不能被挂载到游戏物体上。它是一个纯 C# 类。 - 作用: 当程序第一次用到
UIManager时,它就会自动new出一个唯一的实例。这种写法非常安全,自带线程安全特性,且不需要像 MonoBehaviour 单例那样去场景里找物体。
2. 泛型与反射机制的巧妙结合 (Generics & Reflection)
- 知识点:
typeof(T).Name - 作用: 这是一种极其优雅的“约定优于配置”的设计!
- 当你调用
UIManager.Instance.show()时,typeof(T).Name会自动把类名转换成字符串"LoginPanel"。 - 然后它利用这个字符串去
Resources/UI/文件夹下找名字一模一样的 Prefab(预制体)。 - 好处: 你永远不需要手动去配置“哪个脚本对应哪个预制体”,只要保证脚本名字和预制体名字完全一致,系统就能自动加载。
- 当你调用
3. 字典:高效的缓存管理 (Dictionary Data Structure)
- 知识点:
Dictionary panelDic - 作用: 字典通过“键值对(Key-Value)”来存储数据。这里用面板的名字作 Key,面板脚本的引用作 Value。
- 当你想隐藏面板时,不需要用耗性能的
GameObject.Find去场景里找,直接通过panelDic["LoginPanel"]就能瞬间拿到它。时间复杂度是 O(1),极其高效。
- 当你想隐藏面板时,不需要用耗性能的
4. 完美闭环:回调函数实战 (Callback Implementation
知识点: 在
HidePanel中传入 Lambda 表达式。panelDic[panelName].HideMe(() =>
{
GameObject.Destroy(panelDic[panelName].gameObject);
panelDic.Remove(panelName);
});原理解析: 这就是前面的
BasePanel里那个callback的真面目!UIManager 告诉面板:“你先去播你的淡出动画(HideMe),我这里写好了一个匿名函数(销毁物体+清理字典),交给你带着。等你动画播完彻底黑掉的那一帧,你帮我调用一下这段逻辑。” 这样就完美实现了“先平滑淡出,再彻底销毁”的视觉体验。
