UI框架从0到1,第五节

        在上一节,我们完善了一下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、值可能是引用类型(如 stringGameObject、自定义对象等),用结构体传这些时也只是引用的复制,节省不了什么;

        我们继续,有了这个消息体之后我们要干什么呢?

首先,我们的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 来执行一段响应事件的逻辑就够了。

想深入理解的同学可以去了解:

  • 什么是 ActionFunc

  • 什么是 委托(Delegate)

  • 什么是 事件监听机制

        OK,现在我们可以使用事件名来进行判断了,名为OnClick的事件即为ButtonCustom发送的,名为OnToggleValueChanged的事件就是ToggleCustom发送的。现在Button组件可以和Toggle组件重名而不影响逻辑处理(但同类组件还是不可重名,这个我们后面会解决这个问题),并且我们也可以很便捷的获取到每一种UI控件传递的值。各位小伙伴也可以根据自己的喜好对不同的UI控件的脚本进行自定义事件,这里不再赘述。

        那么现在我们的框架终于有了一点起色了,细心的小伙伴可能会纳闷,UI 控件要传递的不就是点击一下、是否勾选(bool)、输入的文字(string)这种简单的值吗?为什么我们要把事件消息体设计得这么复杂、还搞什么泛型上下文?在下一节,我们就来回答这个疑问,并顺便通过代码演示,如何用这个事件系统,轻松实现 MVVM(或更准确地说,类 MVVM)风格的数据绑定思想

UI框架从0到1,第六节-CSDN博客

你可能感兴趣的:(unity,ui)