在上一节,我们完善了一下PanelBase的继承关系,现在我们要开发一个复杂的需求,我们要在HelloWorldPanel脚本里判断发来事件的UI是哪种UI,如果有值的话还要便捷的拿到其传递的值。那么要怎么做呢?
首先,我们定义一个类作为消息体,当我们要在事件中传递值的时候,只要实例化一个消息体就可以传递各种信息了。
///
/// 所有事件上下文的通用接口,用于标识类型
///
public interface IEventContext { }
///
/// 泛型事件上下文,表示某个值的变化(旧值 -> 新值)
///
/// 值的类型
public class ValueChangedContext : IEventContext
{
///
/// 原始值
///
public T OldValue;
///
/// 当前值
///
public T NewValue;
public ValueChangedContext(T oldVal, T newVal)
{
OldValue = oldVal;
NewValue = newVal;
}
}
有些同学可能会说,频繁创建的东西应该使用结构体而不是类,很好,有性能优化意识是非常棒的,但是此处不用结构体,原因有三:
1、消息体并不是仅仅UI交互事件会调用,我们后面还会有更丰富的使用场景,结构体不适合做多态用法;
2、如果用结构体实现接口(如 IEventContext
),在传递时容易发生装箱,反而产生 GC;
3、值可能是引用类型(如 string
、GameObject
、自定义对象等),用结构体传这些时也只是引用的复制,节省不了什么;
我们继续,有了这个消息体之后我们要干什么呢?
首先,我们的UI组件脚本当然是要修改发送消息的方式,统一使用这个消息体来发送,对于button这种没有值传递的可以不用发送上下文。
using UnityEngine;
///
/// 所有 UI 控件的基类(按钮、输入框等)
/// 它的职责是:
/// 1. 自动向上查找并绑定所属面板(PanelBase)
/// 2. 提供统一的事件发送接口(Send)
///
public class EventUIBase : Base
{
// 缓存本控件所属的面板(负责处理这个控件发出的事件)
protected PanelBase targetPanel;
///
/// 初始化函数(由 Base 框架调用)
/// 用于自动向上查找 PanelBase 组件并绑定
///
protected virtual void Init()
{
// 从当前控件开始,逐层往上遍历父物体
Transform currentTransform = transform;
while (currentTransform != null)
{
// 尝试获取当前物体上的 PanelBase 脚本
targetPanel = currentTransform.GetComponent();
// 如果找到了,保存并结束查找
if (targetPanel != null)
{
return;
}
// 没找到,继续向上查找父节点
currentTransform = currentTransform.parent;
}
// 如果最终都没找到,输出警告(说明你这个控件没有挂在任何面板下)
Debug.LogWarning("No parent with PanelBase found.");
}
///
/// 向所属面板发送一个事件(带事件名 + 上下文数据)
///
protected void Send(string eventName, IEventContext context)
{
// 调用面板的 ProcessEvent 方法,传入事件名、发送者(this)、上下文
targetPanel?.ProcessEvent(eventName, this, context);
}
///
/// 向所属面板发送一个事件(只有事件名,不携带数据)
///
protected void Send(string eventName)
{
// 调用面板的 ProcessEvent 方法(没有上下文参数)
targetPanel?.ProcessEvent(eventName, this);
}
}
using UnityEngine;
using UnityEngine.UI;
///
/// ButtonCustom 是框架中所有按钮的通用逻辑脚本
/// 它继承自 EventUIBase,负责:
/// 1. 初始化时绑定按钮点击事件
/// 2. 点击时向上发送统一事件("OnClick")
///
public class ButtonCustom : EventUIBase
{
// 缓存 Button 组件
Button btn;
///
/// 初始化方法
///
protected override void Init()
{
// 调用父类 Init,查找所属 PanelBase
base.Init();
// 获取当前物体上的 Button 组件
btn = GetComponent
using UnityEngine.UI;
///
/// ToggleCustom 是Toggle控件的通用脚本
/// 它继承自 EventUIBase,主要功能是:
/// 1. 初始化时绑定 Toggle 的值变更事件
/// 2. 当用户切换开关状态时,向所属面板发送事件,并附带旧值与新值
///
public class ToggleCustom : EventUIBase
{
// 当前物体上的 Toggle 组件引用
Toggle tog;
// 记录上一次的开关状态
private bool previousValue;
///
/// 初始化函数(由框架自动调用)
///
protected override void Init()
{
// 调用基类的 Init 方法,绑定所属 PanelBase
base.Init();
// 获取当前 GameObject 上的 Toggle 组件
tog = GetComponent();
// 初始化 previousValue,记录初始状态
previousValue = tog.isOn;
// 监听 Toggle 状态变更事件
tog.onValueChanged.AddListener((newValue) =>
{
// 每次值变化时,发送事件名为 OnToggleValueChanged 的事件
// 附带的上下文是旧值 → 新值(通过 ValueChangedContext 包装)
Send("OnToggleValueChanged", new ValueChangedContext(previousValue, newValue));
// 更新旧值为当前值,准备下次比较
previousValue = newValue;
});
}
}
当然,与此同时PanelBase的ProcessEvent也需要进行大修大改。还是一样,我们在注释里进行逐行讲解。
using System;
using UnityEngine;
///
/// PanelBase 是框架中所有 UI 面板的基类
/// 它负责:
/// 1. 接收控件发来的事件(通过 ProcessEvent)
/// 2. 判断事件类型,并做出响应(如显示、隐藏)
/// 3. 提供通用的事件派发工具函数(Dispatch)
///
public class PanelBase : MonoBehaviour
{
///
/// UI 控件统一调用这个方法来发送事件给面板处理
/// eventName:事件名称(如 "OnClick")
/// sender:哪个控件发来的(可选)
/// context:携带的上下文数据(如 ValueChangedContext)
///
public virtual void ProcessEvent(string eventName, EventUIBase sender, IEventContext context = null)
{
// 打印日志,方便调试看事件有没有收到
Debug.Log($"[{this.name}] 收到事件: {eventName}");
// 示例事件处理:监听 "IsShow" 事件,处理面板显示/隐藏
Dispatch(eventName, "IsShow", context, (oldVal, newVal) =>
{
Debug.Log($"IsShow 从 {oldVal} 变为 {newVal}");
if (newVal)
OnShow(); // 显示面板
else
OnClose(); // 隐藏面板
});
// 其他事件可由子类重写 ProcessEvent 来扩展
}
///
/// 泛型事件分发方法(带上下文数据)
/// 如果事件名匹配 targetEvent,并且 context 是 ValueChangedContext 类型,则执行回调事件
///
protected void Dispatch(string eventName, string targetEvent, IEventContext context, Action callback)
{
if (eventName == targetEvent && context is ValueChangedContext valCtx)
{
callback?.Invoke(valCtx.OldValue, valCtx.NewValue);
}
}
///
/// 简单事件分发方法(不带上下文数据)
/// 用于像 "OnClick" 这种不需要附加数据的事件
///
protected void Dispatch(string eventName, string targetEvent, Action callback)
{
if (eventName == targetEvent)
{
callback?.Invoke();
}
}
protected void OnShow()
{
transform.localScale = Vector3.one;
}
protected void OnClose()
{
transform.localScale = Vector3.up;
}
}
using UnityEngine;
///
/// HelloWorldPanel 是一个具体的 UI 面板类
/// 用于处理本面板接收到的所有 UI 控件事件
///
public class HelloWorldPanel : PanelBase
{
///
/// 重写 ProcessEvent 方法,处理本面板的专属逻辑
///
/// 事件名(如 "OnClick", "OnToggleValueChanged")
/// 事件发送者(控件或视图)
/// 附带的上下文数据(可选)
public override void ProcessEvent(string eventName, Base sender, IEventContext context = null)
{
// 调用基类处理通用事件(如 IsShow)
base.ProcessEvent(eventName, sender, context);
// 处理按钮点击事件
Dispatch(eventName, "OnClick", () =>
{
switch (sender.name)
{
case "按钮1":
Debug.Log("HelloWorldPanel 处理了 按钮1");
break;
default:
Debug.LogWarning($"未处理的按钮点击事件:{sender.name}");
break;
}
});
// 处理 Toggle 状态变更事件
Dispatch(eventName, "OnToggleValueChanged", context, (oldVal, newVal) =>
{
switch (sender.name)
{
case "toggle1":
Debug.Log($"HelloWorldPanel 处理了 toggle1,值变为 {newVal}");
break;
default:
Debug.LogWarning($"未处理的 Toggle 事件:{sender.name}");
break;
}
});
}
}
这一节开始加速了,建议小伙伴们如果看得有点晕的话可以先把代码抄下去用着,有空的时候多琢磨琢磨。这里用到了一个叫做 Action
的东西,也就是匿名委托,它的本质就是“把一段函数逻辑打包起来,作为参数传给另一个函数”。
Dispatch(eventName, "OnClick", () => {
Debug.Log("按钮被点击了!");
});
这一段的意思是:当 eventName
恰好等于 "OnClick"
,就执行我们给的这段代码块。
你可以把它理解成:“我现在不做这件事,我只是先把‘要做的事’写好交给你,到时候你条件满足了再帮我执行。”,这也是事件监听机制的核心思想。
另外,Dispatch()
方法其实就是一个“事件判断器”+“事件执行器”的组合。它的逻辑大概是:“如果收到的事件名和目标事件名一致,那我就执行你给我的代码。”
这种写法虽然一开始看着有点怪,但它其实帮我们简化了很多 if/else 判断,让代码更清晰、可维护性更高。
最后提醒一句:
初学者不需要立刻搞懂每个细节,现在只需要理解:你可以通过 Dispatch
来执行一段响应事件的逻辑就够了。
想深入理解的同学可以去了解:
什么是 Action
、Func
;
什么是 委托(Delegate);
什么是 事件监听机制;
OK,现在我们可以使用事件名来进行判断了,名为OnClick的事件即为ButtonCustom发送的,名为OnToggleValueChanged的事件就是ToggleCustom发送的。现在Button组件可以和Toggle组件重名而不影响逻辑处理(但同类组件还是不可重名,这个我们后面会解决这个问题),并且我们也可以很便捷的获取到每一种UI控件传递的值。各位小伙伴也可以根据自己的喜好对不同的UI控件的脚本进行自定义事件,这里不再赘述。
那么现在我们的框架终于有了一点起色了,细心的小伙伴可能会纳闷,UI 控件要传递的不就是点击一下、是否勾选(bool)、输入的文字(string)这种简单的值吗?为什么我们要把事件消息体设计得这么复杂、还搞什么泛型上下文?在下一节,我们就来回答这个疑问,并顺便通过代码演示,如何用这个事件系统,轻松实现 MVVM(或更准确地说,类 MVVM)风格的数据绑定思想。
UI框架从0到1,第六节-CSDN博客