# 观前提醒
项目会在 GitHub 中开源,链接:https://github.com/Maikire/Unity/tree/main/UnityFramework/A simple Unity UI framework
# UI 框架
这是一个简单的 UI 框架,用于统一管理 UI 和 UI 事件
# 需求分析
- UI 窗口(Canvas)的统一管理(记录 / 隐藏所有窗口,提供查找窗口的方法)
- UI 事件管理
- UI 流程控制(例如:打开多个 UI 窗口时,按下 Esc 关闭最后打开的窗口)
# 核心类
- UI 管理类 UIManager
用于管理(记录 / 隐藏)所有窗口,提供查找窗口的方法 - UI 窗口类 UIWindow
所有 UI 窗口的基类,可以代表所有窗口(概念继承,以层次化方式管理类)、定义所有窗口共有的行为(比如:显示和隐藏) - UI 事件监听器 UIEventListener
提供所有能用到的 UI 事件(带事件参数类) - UI 控制器 UIController
处理 UI 流程,存储打开的 UI
为什么要写UI事件监听器?
因为 Button 组件有几个致命的问题:
- Button 只有单击事件,而其他大多数事件(光标按下、光标抬起...)都不具备。
- Button 没有事件参数类(当多个按钮绑定同一个方法时,没有事件参数,就无法确定是哪个按钮被按下了)。
# 类图
# 代码部分
# UI 管理器
- 只需要一个 UIManager 的实例,所以可以使用单例模式
- 使用字典来储存所有的 UI
- 提供查找、添加的功能
代码如下:
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 事件监听器
代码如下:
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 () 订阅事件
新增事件的代码如下:
using UnityEngine.EventSystems; | |
namespace Default | |
{ | |
public interface IPointerPressHandler : IEventSystemHandler | |
{ | |
/// <summary> | |
/// 长按按钮 | |
/// </summary> | |
/// <param name="eventData">PointerEventData</param> | |
void OnPointerPress(PointerEventData eventData); | |
} | |
} |
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 事件监听器的代码如下:
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(关闭当前的窗口 = 关闭最后一个打开的窗口 = 获取栈顶并对其进行关闭操作)
代码如下:
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 输入控制器
- 管理不同的输入方式(本文以键盘输入为例)
代码如下:
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,如图所示:
- “UI” 的组件如图所示:
- Canvas 的组件如图所示:
- 按钮的组件如图所示:
- “Back” 是背景板
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 的代码:
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 的代码:
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); | |
} | |
} | |
} |