# 观前提醒

项目会在 GitHub 中开源,链接:https://github.com/Maikire/Unity/tree/main/UnityFramework/A simple Unity UI framework

# UI 框架

这是一个简单的 UI 框架,用于统一管理 UI 和 UI 事件

# 需求分析

  1. UI 窗口(Canvas)的统一管理(记录 / 隐藏所有窗口,提供查找窗口的方法)
  2. UI 事件管理
  3. UI 流程控制(例如:打开多个 UI 窗口时,按下 Esc 关闭最后打开的窗口)

# 核心类

  1. UI 管理类 UIManager
    用于管理(记录 / 隐藏)所有窗口,提供查找窗口的方法
  2. UI 窗口类 UIWindow
    所有 UI 窗口的基类,可以代表所有窗口(概念继承,以层次化方式管理类)、定义所有窗口共有的行为(比如:显示和隐藏)
  3. UI 事件监听器 UIEventListener
    提供所有能用到的 UI 事件(带事件参数类)
  4. UI 控制器 UIController
    处理 UI 流程,存储打开的 UI
为什么要写UI事件监听器?

因为 Button 组件有几个致命的问题

  • Button 只有单击事件,而其他大多数事件(光标按下、光标抬起...)都不具备。
  • Button 没有事件参数类(当多个按钮绑定同一个方法时,没有事件参数,就无法确定是哪个按钮被按下了)。

# 类图

img

# 代码部分

# UI 管理器

  • 只需要一个 UIManager 的实例,所以可以使用单例模式
  • 使用字典来储存所有的 UI
  • 提供查找、添加的功能

代码如下:

UIManager
using Common;
using System.Collections.Generic;
using UnityEditor.PackageManager.UI;
namespace Default
{
    /// <summary>
    /// UI 管理器:管理(记录 / 隐藏)所有窗口,提供查找窗口的方法
    /// </summary>
    public class UIManager : MonoSingleton<UIManager>
    {
        /// <summary>
        /// 窗口对象字典
        ///key: 窗口对象名称
        ///value: 窗口对象引用
        /// </summary>
        private Dictionary<string, UIWindow> UIWindowDIC;
        private void Start()
        {
            InitDic();
        }
        /// <summary>
        /// 初始化字典
        /// </summary>
        private void InitDic()
        {
            UIWindowDIC = new Dictionary<string, UIWindow>();
            foreach (var item in this.GetComponentsInChildren<UIWindow>())
            {
                UIWindowDIC.Add(item.GetType().Name, item);
                if (!item.IsBasicUICanvas)
                {
                    item.gameObject.SetActive(false);
                }
            }
        }
        /// <summary>
        /// 根据类型查找窗口
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T GetWindow<T>() where T : UIWindow
        {
            string T_Name = typeof(T).Name;
            if (!UIWindowDIC.ContainsKey(T_Name)) return null;
            return UIWindowDIC[T_Name] as T;
        }
    }
}

# UI 窗口类

  • 所有窗口的父类
  • 使用 CanvasGroup 统一调节透明度(以此实现淡入淡出)
  • 提供开关 UI 的方法,将 UI 入栈 / 出栈(栈在单例类 UIController 中定义,后面会讲到)
  • 提供查找 UI 事件监听器的方法(原因:UI 窗口查找 UI 事件监听器往往会有很多次,所以将获取 UI 事件监听器的方法封装到父类)
  • 使用变换组件助手类查找 UI 事件监听器

代码如下:

UIWindow
using Common;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Default
{
    [RequireComponent(typeof(CanvasGroup))]
    /// <summary>
    /// 所有窗口的父类
    /// </summary>
    public abstract class UIWindow : MonoBehaviour
    {
        /// <summary>
        /// 画布组
        /// </summary>
        protected CanvasGroup UICanvasGroup;
        /// <summary>
        /// 存储 UI 事件监听器
        /// </summary>
        private Dictionary<string, UIEventListener> UIEventDic;
        /// <summary>
        ///true: 是基础的 UI 画布(打开场景时基础 UI 画布不会被隐藏,其他 UI 画布会被隐藏)
        /// </summary>
        public bool IsBasicUICanvas { get; protected set; }
        public virtual void Awake()
        {
            UICanvasGroup = this.GetComponent<CanvasGroup>();
            UIEventDic = new Dictionary<string, UIEventListener>();
        }
        /// <summary>
        /// 设置 UI 的可见性
        /// </summary>
        /// <param name="state">true:设置为可见 & lt;/param>
        public void SetVisible(bool state)
        {
            this.gameObject.SetActive(state);
            if (IsBasicUICanvas) return;
            if (state)
            {
                UIController.Instance.UICanvasStack.Push(this);
            }
            else
            {
                UIController.Instance.UICanvasStack.Pop();
            }
        }
        /// <summary>
        /// 在一定时间内(秒),将 UI 的透明度从指定的值平滑过度到另一个指定的值
        /// </summary>
        /// <param name="sourceAlpha"> 起始 alpha</param>
        /// <param name="targetAlpha"> 目标 alpha</param>
        /// <param name="time"> 时间范围(秒)</param>
        /// <returns></returns>
        public void SetVisibleAlpha(float sourceAlpha, float targetAlpha, float time)
        {
            StartCoroutine(SetVisibleAlphaDelay(sourceAlpha, targetAlpha, time));
        }
        /// <summary>
        /// 在一定时间内(秒),将 UI 的透明度从指定的值平滑过度到另一个指定的值
        /// </summary>
        /// <param name="sourceAlpha"> 起始 alpha</param>
        /// <param name="targetAlpha"> 目标 alpha</param>
        /// <param name="time"> 时间范围(秒)</param>
        /// <returns></returns>
        private IEnumerator SetVisibleAlphaDelay(float sourceAlpha, float targetAlpha, float time)
        {
            UICanvasGroup.alpha = sourceAlpha;
            float difference = targetAlpha - UICanvasGroup.alpha;
            if (!this.gameObject.activeSelf && targetAlpha > 0)
            {
                SetVisible(true);
            }
            while (true)
            {
                yield return new WaitForSeconds(0.02f);
                UICanvasGroup.alpha += difference / (time * 50);
                if (UICanvasGroup.alpha <= 0)
                {
                    SetVisible(false);
                    yield break;
                }
                else if (UICanvasGroup.alpha == targetAlpha)
                {
                    yield break;
                }
            }
        }
        /// <summary>
        /// 查找 UI 事件监听器
        /// </summary>
        /// <param name="UIName"></param>
        /// <returns></returns>
        public UIEventListener GetUIEventListener(string UIName)
        {
            if (!UIEventDic.ContainsKey(UIName))
            {
                UIEventListener uiEventListener = UIEventListener.GetListener(this.transform.FindChildByName(UIName));
                UIEventDic.Add(UIName, uiEventListener);
                return uiEventListener;
            }
            else
            {
                return UIEventDic[UIName];
            }
        }
    }
}

# UI 事件监听器

  • 提供所有能用到的 UI 事件(带事件参数类)
    在原有事件的基础上增加一个事件:按下按钮后一直触发
  • 实现 UGUI 的接口
  • 提供获取 UI 事件监听器的方法(如果没有,就添加一个)
  • 使用 Unity 事件(也可以用委托,本文以 Unity 事件为例)
  • 使用 AddListener () 订阅事件

新增事件的代码如下:

IPointerPressHandler
using UnityEngine.EventSystems;
namespace Default
{
    public interface IPointerPressHandler : IEventSystemHandler
    {
        /// <summary>
        /// 长按按钮
        /// </summary>
        /// <param name="eventData">PointerEventData</param>
        void OnPointerPress(PointerEventData eventData);
    }
}
PointerPressManger
using UnityEngine;
using UnityEngine.EventSystems;
namespace ARPGDemo.UI
{
    /// <summary>
    /// 按下按钮时一直执行
    /// </summary>
    public class PointerPressManger : MonoBehaviour
    {
        private UIEventListener EventListener;
        private PointerEventData EventData;
        private void Awake()
        {
            EventListener = this.GetComponent<UIEventListener>();
        }
        private void Start()
        {
            EventListener.onPointerDown.AddListener(OnTurnOn);
            EventListener.onPointerUp.AddListener(OnTurnOff);
            this.enabled = false;
        }
        private void Update()
        {
            EventListener.OnPointerPress(EventData);
        }
        /// <summary>
        /// 开启循环调用事件
        /// </summary>
        /// <param name="eventData"></param>
        private void OnTurnOn(PointerEventData eventData)
        {
            EventData = eventData;
            this.enabled = true;
        }
        /// <summary>
        /// 关闭循环调用事件
        /// </summary>
        /// <param name="eventData"></param>
        private void OnTurnOff(PointerEventData eventData)
        {
            EventData = eventData;
            this.enabled = false;
        }
    }
}

UI 事件监听器的代码如下:

UIEventListener
using System;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
namespace ARPGDemo.UI
{
    [RequireComponent(typeof(PointerPressManger))]
    /// <summary>
    /// UI 事件监听器,提供所有能用到的 UGUI 事件(带事件参数类)。
    /// 脚本添加到需要交互的 UI
    /// </summary>
    public class UIEventListener : MonoBehaviour, IPointerPressHandler, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler, IInitializePotentialDragHandler, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IScrollHandler, IUpdateSelectedHandler, ISelectHandler, IDeselectHandler, IMoveHandler, ISubmitHandler, ICancelHandler, IEventSystemHandler
    {
        [Serializable]
        public class PointerEventHandler : UnityEvent<PointerEventData> { }
        [Serializable]
        public class BaseEventHandler : UnityEvent<BaseEventData> { }
        [Serializable]
        public class AxisEventHandler : UnityEvent<AxisEventData> { }
        // 使用了 C# 9 新引入的 Target-typed new 表达式语法。省略对构造函数的显式调用
        public PointerEventHandler onPointerPress = new();
        public PointerEventHandler onPointerClick = new();
        public PointerEventHandler onPointerDown = new();
        public PointerEventHandler onPointerUp = new();
        public PointerEventHandler onPointerEnter = new();
        public PointerEventHandler onPointerExit = new();
        public PointerEventHandler onInitializePotentialDrag = new();
        public PointerEventHandler onBeginDrag = new();
        public PointerEventHandler onDrag = new();
        public PointerEventHandler onEndDrag = new();
        public PointerEventHandler onDrop = new();
        public PointerEventHandler onScroll = new();
        public BaseEventHandler onUpdateSelected = new();
        public BaseEventHandler onSelect = new();
        public BaseEventHandler onDeselect = new();
        public BaseEventHandler onSubmit = new();
        public BaseEventHandler onCancel = new();
        public AxisEventHandler onMove = new();
        private void Start()
        {
            this.TryGetComponent<Button>(out Button button);
            if (button != null)
            {
                button.onClick.AddListener(() => { onPointerClick?.Invoke(null); });
            }
        }
        /// <summary>
        /// 获取 UI 事件监听器
        /// </summary>
        /// <param name="transform"></param>
        /// <returns></returns>
        public static UIEventListener GetListener(Transform transform)
        {
            UIEventListener uiEventListener = transform.GetComponent<UIEventListener>();
            if (uiEventListener == null)
            {
                uiEventListener = transform.AddComponent<UIEventListener>();
            }
            return uiEventListener;
        }
        public void OnPointerPress(PointerEventData eventData)
        {
            onPointerPress?.Invoke(eventData);
        }
        public void OnPointerClick(PointerEventData eventData)
        {
            onPointerClick?.Invoke(eventData);
        }
        public void OnPointerDown(PointerEventData eventData)
        {
            onPointerDown?.Invoke(eventData);
        }
        public void OnPointerUp(PointerEventData eventData)
        {
            onPointerUp?.Invoke(eventData);
        }
        public void OnPointerEnter(PointerEventData eventData)
        {
            onPointerEnter?.Invoke(eventData);
        }
        public void OnPointerExit(PointerEventData eventData)
        {
            onPointerExit?.Invoke(eventData);
        }
        public void OnInitializePotentialDrag(PointerEventData eventData)
        {
            onInitializePotentialDrag?.Invoke(eventData);
        }
        public void OnBeginDrag(PointerEventData eventData)
        {
            onBeginDrag?.Invoke(eventData);
        }
        public void OnDrag(PointerEventData eventData)
        {
            onDrag?.Invoke(eventData);
        }
        public void OnEndDrag(PointerEventData eventData)
        {
            onEndDrag?.Invoke(eventData);
        }
        public void OnDrop(PointerEventData eventData)
        {
            onDrop?.Invoke(eventData);
        }
        public void OnScroll(PointerEventData eventData)
        {
            onScroll?.Invoke(eventData);
        }
        public void OnUpdateSelected(BaseEventData eventData)
        {
            onUpdateSelected?.Invoke(eventData);
        }
        public void OnSelect(BaseEventData eventData)
        {
            onSelect?.Invoke(eventData);
        }
        public void OnDeselect(BaseEventData eventData)
        {
            onDeselect?.Invoke(eventData);
        }
        public void OnSubmit(BaseEventData eventData)
        {
            onSubmit?.Invoke(eventData);
        }
        public void OnCancel(BaseEventData eventData)
        {
            onCancel?.Invoke(eventData);
        }
        public void OnMove(AxisEventData eventData)
        {
            onMove?.Invoke(eventData);
        }
    }
}

# UI 控制器

  • 处理 UI 的基本流程
  • 使用栈储存已经打开的 UI(关闭当前的窗口 = 关闭最后一个打开的窗口 = 获取栈顶并对其进行关闭操作)

代码如下:

UIController
using Common;
using System.Collections.Generic;
namespace Default
{
    /// <summary>
    /// 处理 UI 流程
    /// </summary>
    public class UIController : MonoSingleton<UIController>
    {
        /// <summary>
        /// 将打开的 UI 入栈
        /// </summary>
        public Stack<UIWindow> UICanvasStack;
        private void Start()
        {
            UICanvasStack = new Stack<UIWindow>();
        }
        /// <summary>
        /// 返回上一层 UI
        /// 如果没有上一层 UI,则什么都不做
        /// </summary>
        public void Back()
        {
            if (UICanvasStack.Count != 0)
            {
                UICanvasStack.Peek().SetVisible(false);
            }
        }
    }
}

# UI 输入控制器

  • 管理不同的输入方式(本文以键盘输入为例)

代码如下:

UIInputController
using Common;
using UnityEngine;
namespace Default
{
    /// <summary>
    /// UI 输入控制器
    /// </summary>
    public class UIInputController : MonoSingleton<UIInputController>
    {
        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Escape))
            {
                UIController.Instance.Back();
            }
        }
    }
}

# UI 框架测试

框架已经完成了,接下来开始测试

# 创建 UI

  • 创建 UI,如图所示:
    img
  • “UI” 的组件如图所示:
    img
  • Canvas 的组件如图所示:
    img
  • 按钮的组件如图所示:
    img
  • “Back” 是背景板

UIStartWindow 的代码:

UIStartWindow
using UnityEngine.EventSystems;
namespace Default
{
    /// <summary>
    /// 游戏开始界面
    /// </summary>
    public class UIStartWindow : UIWindow
    {
        public override void Awake()
        {
            base.Awake();
            IsBasicUICanvas = true;
        }
        // 游戏开始时,注册 UI 交互事件
        private void Start()
        {
            GetUIEventListener("Start").PointerClick.AddListener(OnStartButtonClick);
            GetUIEventListener("Setting").PointerClick.AddListener(OnSettingButtonClick);
        }
        /// <summary>
        /// 提供当前面板负责的交互行为
        /// </summary>
        private void OnStartButtonClick(PointerEventData eventData)
        {
            SetVisibleAlpha(1, 0, 0.2f);
        }
        /// <summary>
        /// 进入设置面板
        /// </summary>
        private void OnSettingButtonClick(PointerEventData eventData)
        {
            UIManager.Instance.GetWindow<UISettingWindow>().SetVisible(true);
        }
    }
}

UISettingWindow 的代码:

UISettingWindow
using UnityEngine.EventSystems;
namespace Default
{
    /// <summary>
    /// 游戏设置界面
    /// </summary>
    public class UISettingWindow : UIWindow
    {
        private void Start()
        {
            GetUIEventListener("Exit").PointerClick.AddListener(OnExitButtonClick);
            GetUIEventListener("Setting_1").PointerClick.AddListener(OnSetting_1ButtonClick);
        }
        /// <summary>
        /// 退出画布
        /// </summary>
        private void OnExitButtonClick(PointerEventData eventData)
        {
            this.SetVisible(false);
        }
        /// <summary>
        /// 打开 Setting_1 
        /// </summary>
        /// <param name="eventData"></param>
        private void OnSetting_1ButtonClick(PointerEventData eventData)
        {
            UIManager.Instance.GetWindow<UISettingWindow_1>().SetVisible(true);
        }
    }
}

UISettingWindow_1 的代码:

UISettingWindow_1
using UnityEngine.EventSystems;
namespace Default
{
    /// <summary>
    /// 设置面板中的一个选项的面板
    /// </summary>
    public class UISettingWindow_1 : UIWindow
    {
        private void Start()
        {
            GetUIEventListener("Exit").PointerClick.AddListener(OnExitButtonClick);
        }
        /// <summary>
        /// 退出画布
        /// </summary>
        private void OnExitButtonClick(PointerEventData eventData)
        {
            this.SetVisible(false);
        }
    }
}

# 最终结果

img