8.UI模块

8.UI模块


8.1 知识点

UI模块的作用

在游戏开发中,UI管理是一个非常重要的环节。我们经常需要创建大量的UI面板,每个面板都需要:

  1. 拼面板:在Unity编辑器中搭建UI界面(必须做)
  2. 声明组件:在代码中声明各种UI组件变量(重复工作)
  3. 查找组件:通过代码获取UI控件的引用(重复工作)
  4. 监听事件:为按钮、滑动条等添加事件监听(重复工作)
  5. 处理逻辑:实现UI的具体业务逻辑(必须做)

核心问题:

  • 每次制作UI面板时,步骤2、3、4都需要重复编写大量冗余代码
  • 缺乏统一的UI管理机制,面板的显示隐藏逻辑分散
  • 面板之间缺乏层级管理,UI显示顺序混乱

解决方案:
通过创建UI管理模块,提供以下功能:

  1. UI面板基类(BasePanel):自动查找组件、监听事件,无需重复编写冗余代码
  2. UI管理器(UIMgr):统一管理所有UI面板的显示隐藏,支持层级管理
  3. 自动事件管理:通过命名规则自动为组件添加事件监听
  4. 资源管理整合:与资源管理模块整合,支持同步和异步加载面板

主要功能:

  1. 自动组件查找:通过命名规则自动查找并管理UI组件
  2. 自动事件监听:为Button、Slider、Toggle等组件自动添加事件监听
  3. 面板管理:统一管理面板的创建、显示、隐藏和销毁
  4. 层级管理:支持Bottom、Middle、Top、System四层UI管理
  5. 异步加载:支持面板异步加载,避免卡顿
  6. 可选销毁:支持失活和销毁两种模式,提高性能

UI模块的基本原理

设计理念:

  • 自动化:通过命名规则和反射机制自动查找组件
  • 统一管理:通过UIMgr统一管理所有面板的生命周期
  • 层级分离:通过Canvas层级实现UI的前后关系管理
  • 资源整合:整合资源管理模块,支持AB包加载
  • 状态管理:使用PanelInfo管理面板的加载状态和回调

工作流程:

  1. 面板创建:通过UIMgr.ShowPanel创建面板,从AB包异步加载
  2. 组件查找:BasePanel在Awake中自动查找所有子组件
  3. 事件监听:根据组件类型自动添加相应的事件监听
  4. 面板显示:调用ShowMe方法,执行自定义逻辑
  5. 面板隐藏:调用HideMe方法,可选择失活或销毁
  6. 自动清理:失活的面板在再次显示时可复用

UI面板基类实现

为什么需要UI面板基类

传统的UI制作流程中,声明组件、查找组件、监听事件这三个步骤每次都重复编写,造成大量冗余代码。通过制作UI面板基类,我们可以:

  1. 自动查找组件:通过命名规则和反射自动获取UI组件引用
  2. 自动添加事件:根据组件类型自动添加相应的事件监听
  3. 统一样式:为所有面板提供统一的获取组件方法

UI面板基类实现思路

主要实现思路:

  1. 制定命名规则:需要使用的组件重命名,不使用的组件保持默认名
  2. 自动查找组件:在Awake中使用GetComponentsInChildren自动查找所有子组件
  3. 自动添加事件:检测组件类型(Button、Slider、Toggle),自动添加事件监听
  4. 提供获取方法:提供GetControl方法,让子类获取指定组件
  5. 虚方法设计:提供ShowMe和HideMe虚方法,供子类重写

设计要点:

  1. 默认名字过滤:准备一个默认名字容器,过滤掉用于显示的组件(如Image、Text的背景子对象)
  2. 组件优先级:优先查找重要组件(Button、Toggle等),避免同一对象上挂多个组件的冲突
  3. 里式替换原则:使用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管理器的目标:

  1. 统一管理:统一管理所有UI面板的显示隐藏
  2. 层级管理:通过Canvas层级实现UI的前后关系
  3. 自动创建:自动创建Canvas、EventSystem等UI基础设施
  4. 资源整合:与资源管理模块整合,支持异步加载面板

UI管理器层级规划

主要思路:

  1. Canvas和EventSystem:UI面板在任何场景都会显示,因此Canvas和EventSystem对象应该过场景不移除,并且保证唯一性和动态创建
  2. UI摄像机:使用独立的UI摄像机渲染UI(可选)
  3. 层级对象:在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管理器基础实现

主要实现内容:

  1. 存储面板的容器:使用Dictionary存储所有面板
  2. 显示面板:加载面板并显示到指定层级
  3. 隐藏面板:隐藏并销毁面板
  4. 获取面板:获取指定面板的引用

源码实现: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("面板已隐藏");
    }
}

异步加载优化

为什么需要异步加载优化

在使用异步加载时,可能会遇到以下问题:

  1. 同一帧连续显示同一面板:会触发多次异步加载,导致重复创建
  2. 同一帧显示又隐藏同一面板:异步加载结束后还会创建面板,造成浪费
  3. 获取面板时如果正在加载中:会返回null,无法获取面板引用

异步加载优化思路

主要制作思路:

  1. 造成问题的关键点:由于异步加载,字典容器中没有及时存储将要显示的面板对象
  2. 解决方案:我们需要在显示面板时,一开始就存储面板的相关信息,这样不管是二次显示还是隐藏,都能够知道是否已经在加载面板了
  3. 分情况考虑
    • 显示相关:若加载中想要显示,应该记录回调,加载结束后统一调用
    • 隐藏相关:若加载中想要隐藏,应该改变标识,加载结束后不创建面板
    • 获取相关:若加载中想要获取,应该等待加载结束后再处理获取逻辑

主要解决的问题:

  1. 同一帧连续显示同一面板,避免重复进行异步加载后回调重复往字典中添加面板数据
  2. 同一帧显示—>隐藏—>显示同一面板,面板能够正常显示
  3. 获取面板时如果正在加载中,等加载结束后再获取处理逻辑

源码实现: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管理器中修改逻辑即可
  • 实现内容
    1. 隐藏面板时,可以选择销毁还是失活
    2. 显示面板时,如果存在直接激活,如果不存在再重新创建

源码实现: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功能时,经常会有这样的需求:

  1. 为一些不带默认事件的控件添加自定义事件,比如Image、Text这些基础组件,想为他们添加点击、拖拽等事件监听
  2. 为一些带默认事件的控件添加自定义事件,比如为Button按钮添加鼠标进入、鼠标移除等事件监听

因此我们完全可以把添加自定义事件封装为一个公共函数,方便为各种控件添加自定义事件。

自定义事件实现思路

主要实现思路:

  1. 为想要添加自定义事件的控件添加EventTrigger组件
  2. 通过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

×

喜欢就点赞,疼爱就打赏