介绍
本文针对VRTK做了一套简单的UI框架。由于VR游戏的UI相对来说比较复杂,普通的UGUI并不能满足要求,所以下面我们自定义一套更适合VR的UI框架,以便于开发和管理。
需求
- UI 画布(Canvas)统一管理(记录、提供显隐功能)。
- UI 事件管理。
类图
核心框架类:
- UI窗口类UIWindow: 所有UI窗口的基类,可以代表所有窗口(概念集成,以层次化方式管理类),定义所有窗口共有行为(显隐)。
- UI管理类UIManager:管理(记录、禁用、查找)窗口,定义所有窗口的共有行为(获取监听器)。
- UI事件监听器类UIEventListener:提供当前UI所有事件(带事件参数类)。
功能控制类:
- 游戏控制器GameController:负责处理游戏流程,例如游戏开始前显示主窗口。
工具类:
- 单例功能类MonoSingleton:提供单例模式,继承此类后可以实现单例。
- 变换组件助手类TransformHelper:提供一些变换组件的帮助方法。
应用类:
- UI主窗口类UIMainWindow:附加到主窗口中,负责处理主窗口逻辑。
UI结构
- 根物体 UIManager
- 窗口 xxxWindow : UIWindow
- 交互元素
- 窗口 xxxWindow : UIWindow
- 交互元素
- 窗口 xxxWindow : UIWindow
类开发
核心框架类
- UI窗口类UIWindow: 所有UI窗口的基类,可以代表所有窗口(概念集成,以层次化方式管理类),定义所有窗口共有行为(显隐)。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VRTK;
using y7play.Common;
namespace y7play.VR.UGUI.Framework
{
/// <summary>
/// UI 窗口基类
/// 定义所有窗口共有成员
/// 提供显隐功能
/// </summary>
public class UIWindow : MonoBehaviour
{
private CanvasGroup canvasGroup;
private VRTK_UICanvas uICanvas;
private Dictionary<string, UIEventListener> uiEventDIC;
private void Awake()
{
canvasGroup = GetComponent<CanvasGroup>();
uICanvas = GetComponent<VRTK_UICanvas>();
uiEventDIC = new Dictionary<string, UIEventListener>();
}
/// <summary>
/// 设置窗口可见性
/// </summary>
/// <param name="state">显隐状态</param>
/// <param name="delay">延时时间,默认为0,当时间为0时会等待一帧后执行</param>
public void SetVisable(bool state, float delay = 0)
{
StartCoroutine(SetVisibleDelay(state, delay));
}
/// <summary>
/// 延时设置窗口可见性
/// 协程方法,在一定延迟后隐藏窗口
/// </summary>
/// <param name="state">显隐状态</param>
/// <param name="delay">延时时间</param>
/// <returns>协程</returns>
private IEnumerator SetVisibleDelay(bool state, float delay)
{
yield return new WaitForSeconds(delay);
// CanvasGroup
canvasGroup.alpha = state ? 1 : 0;
// VRTK UICanvas
uICanvas.enabled = state;
}
/// <summary>
/// 根据子物体名称获取监听组件
/// </summary>
/// <param name="name">变换组件名称</param>
/// <returns></returns>
public UIEventListener GetUIEventListener(string name)
{
if (!uiEventDIC.ContainsKey(name))
{
Transform tf = transform.FindChildByName(name);
UIEventListener uIEvent = UIEventListener.GetListener(tf);
uiEventDIC.Add(name, uIEvent);
}
return uiEventDIC[name];
}
}
}
- UI管理类UIManager:管理(记录、禁用、查找)窗口,定义所有窗口的共有行为(获取监听器)。
using System.Collections.Generic;
namespace y7play.VR.UGUI.Framework
{
/// <summary>
/// UI 管理器
/// </summary>
public class UIManager : MonoSingleton<UIManager>
{
// key窗口类名称 value窗口对象引用
Dictionary<string, UIWindow> uiWindowDIC;
/// <summary>
/// 初始化管理器
/// </summary>
public override void Init()
{
base.Init();
uiWindowDIC = new Dictionary<string, UIWindow>();
UIWindow[] uiWindowArr = FindObjectsOfType<UIWindow>();
for (int i = 0; i < uiWindowArr.Length; i++)
{
// 隐藏窗口
uiWindowArr[i].SetVisable(false);
// 记录窗口
AddWindow(uiWindowArr[i]);
}
}
/// <summary>
/// 添加窗口
/// </summary>
/// <param name="window">需要添加的窗口对象</param>
public void AddWindow(UIWindow window)
{
uiWindowDIC.Add(window.GetType().Name, window);
}
/// <summary>
/// 根据类型查找窗口
/// </summary>
/// <typeparam name="T">需要查找的窗口类型</typeparam>
/// <returns>指定类型的窗口对象</returns>
public T GetWindow<T>() where T : class
{
string key = typeof(T).Name;
if (!uiWindowDIC.ContainsKey(key)) return null;
return uiWindowDIC[key] as T;
}
}
}
- UI事件监听器类UIEventListener:提供当前UI所有事件(带事件参数类)。
using UnityEngine;
using UnityEngine.EventSystems;
namespace y7play.VR.UGUI.Framework
{
/// <summary>
/// 定义委托
/// </summary>
/// <param name="eventData"></param>
public delegate void PointerEventHandler(PointerEventData eventData);
/// <summary>
/// UI事件监听器
/// 管理所有UGUI事件,提供事件参数类。
/// 附加到需要交互的UI元素上,用于监听用户的操作。
/// 类似于EventTrigger
/// </summary>
public class UIEventListener : MonoBehaviour, IPointerDownHandler, IPointerClickHandler, IPointerUpHandler
{
/// <summary>
/// 声明事件
/// </summary>
public event PointerEventHandler PointerClick;
public event PointerEventHandler PointerDown;
public event PointerEventHandler PointerUp;
/// <summary>
/// 通过变换组件获取事件监听器
/// 如果没有该组件,则自动添加该组件
/// </summary>
/// <param name="tf">变换组件</param>
/// <returns></returns>
public static UIEventListener GetListener(Transform tf)
{
UIEventListener uiEvent = tf.GetComponent<UIEventListener>();
if (uiEvent == null) uiEvent = tf.gameObject.AddComponent<UIEventListener>();
return uiEvent;
}
public void OnPointerClick(PointerEventData eventData)
{
// 如果PointerClick不为空,就调用PointerClick方法
PointerClick?.Invoke(eventData);
}
public void OnPointerDown(PointerEventData eventData)
{
// 如果PointerDown不为空,就调用PointerDown方法
PointerDown?.Invoke(eventData);
}
public void OnPointerUp(PointerEventData eventData)
{
// 如果PointerUp不为空,就调用PointerUp方法
PointerUp?.Invoke(eventData);
}
}
}
功能控制类
- 游戏控制器GameController:负责处理游戏流程,例如游戏开始前显示主窗口。
using y7play.VR.UGUI;
using y7play.VR.UGUI.Framework;
namespace y7play
{
/// <summary>
/// 游戏控制器
/// 负责处理游戏流程
/// </summary>
public class GameController : MonoSingleton<GameController>
{
// 游戏开始之前
private void Start()
{
UIManager.Instance.GetWindow<UIMainWindow>().SetVisable(true);
}
// 游戏开始
public void GameStart()
{
// 隐藏开始面板
UIManager.Instance.GetWindow<UIMainWindow>().SetVisable(false);
// 创建敌人
}
// 游戏结束
// 游戏暂停
}
}
工具类
- 单例功能类MonoSingleton:提供单例模式,继承此类后可以实现单例。
using UnityEngine;
namespace y7play
{
/// <summary>
/// Mono脚本单例工具
/// 使用场景:所有在场景中只出现一次的脚本都应该使用此类获取实例。<br />
/// 作用:<br />
/// 1、如果脚本已经被引用到场景中,则在任何类中都可以直接使用该子类的实例。<br />
/// 2、如果脚本未被引用,可以直接使用此类进行引用,无需手动引用。<br />
/// 使用方法:<br />
/// 1、在继承此类时必须将子类类型作为泛型传递给父类。<br />
/// 2、在任意脚本生命周期中,通过子类类型访问Instance即可获取子类实例。<br />
/// </summary>
public class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T>
{
/// <summary>
/// 实例对象,该对象不允许外部访问,在此类外部需要使用Instance的get方法来获取实例
/// </summary>
private static T instance;
/// <summary>
/// 用此方法来获取实例,当此方法第一次被调用时,会主动寻找子类的脚本,如果并未找到,则创建该脚本。
/// </summary>
public static T Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<T>();
if (instance == null)
{
new GameObject("Singleton of " + typeof(T)).AddComponent<T>();
}
else
{
instance.Init();
}
}
return instance;
}
}
protected void Awake()
{
if (instance == null)
{
instance = this as T;
Init();
}
}
/// <summary>
/// 默认初始化方法,子类可以重写此方法进行初始化
/// </summary>
public virtual void Init()
{
}
}
}
- 变换组件助手类TransformHelper:提供一些变换组件的帮助方法。
using UnityEngine;
namespace y7play.Common
{
/// <summary>
/// 变换组件助手类
/// </summary>
public static class TransformHelper
{
/// <summary>
/// 递归查找变换组件
/// </summary>
/// <param name="cuurentTF"></param>
/// <param name="childName"></param>
/// <returns></returns>
public static Transform FindChildByName(this Transform cuurentTF, string childName)
{
Transform child = cuurentTF.Find(childName);
if (child != null) return child;
for (int i = 0; i < cuurentTF.childCount; i++)
{
child = FindChildByName(cuurentTF.GetChild(i), childName);
if (child != null) return child;
}
return null;
}
}
}
应用类
- UI主窗口类UIMainWindow:附加到主窗口中,负责处理主窗口逻辑。
using UnityEngine.EventSystems;
namespace y7play.VR.UGUI
{
/// <summary>
/// 游戏主窗口
/// </summary>
public class UIMainWindow : y7play.VR.UGUI.Framework.UIWindow
{
private void Start()
{
// 给开始游戏按钮添加事件
// 通过Find查找元素需要写死路径,但成功的添加了事件。
// transform.Find("ButtonGameStart").GetComponent<Button>().onClick.AddListener(OnGameStartButtonClick);
// 问题1:通过Find查找后代元素,会写死路径。
// 解决:提供一个在未知层级中查找后代元素的工具方法(TransformHelper.FindChildByName)。
// 这个方法有两种调用方式
// 1、静态方法调用方式
//TransformHelper.FindChildByName(transform, "ButtonGameStart").GetComponent<Button>().onClick.AddListener(OnGameStartButtonClick);
// 2、扩展方法调用方式
//transform.FindChildByName("ButtonGameStart").GetComponent<Button>().onClick.AddListener(OnGameStartButtonClick);
// 问题2:Button只具有单击事件,不具备UGUI提供的其他事件类型。
// Button具有的单击事件类没有事件参数。
// 解决:模拟Button编程思想,定义事件监听类(),提供所有UGUI事件(带事件参数)。
//transform.FindChildByName("ButtonGameStart").GetComponent<UIEventListener>().PointerClick += OnPointerClick;
// 问题3:UI窗口查找功能需要多次使用
// 将获取UI监听器封装到UIWindow中
GetUIEventListener("ButtonGameStart").PointerClick += OnPointerClick;
// 实际上对于一个大型项目来说,单个页面的层级结构可能会十分复杂且庞大,要尽可能的降低查询复杂度。
// 可以将业务按模块(场景)划分,每个模块提供单独的配置文件,用于跟前端UI层级对应。
// 以保证当项目整体层级需要改变时能够通过修改配置文件的方式进行统一修改。
// 不同的场景应该提供不同的默认查找路径,提供默认的查抄方法,并另外提供其他模块的查找方法(这个方法应该尽量不对业务开发人员开放,避免程序健壮性受到影响)。
// 不同的场景应该提供动态加载的功能,在场景切换时进行统一的加载和卸载,以保证合理的内存占用。
}
private void OnPointerClick(PointerEventData eventData)
{
// 测试事件参数
print(eventData.pointerPress);
// 调用游戏开始方法
GameController.Instance.GameStart();
// 双击判断
//if (eventData.clickCount == 2)
//{
// GameController.Instance.GameStart();
//}
}
private void OnGameStartButtonClick()
{
// 调用游戏开始方法
GameController.Instance.GameStart();
}
}
}
使用方法
- 定义UIXXXWindow类,继承自UIWindow,负责处理该窗口逻辑。通过GetUIEventListener获取需要交互的UI元素。
- 通过UIManager.Instance.GetWindow<窗口类型>().方法()访问窗口的成员。
进一步改良
实际上对于一个大型项目来说,单个页面的层级结构可能会十分复杂且庞大,要尽可能的降低查询复杂度。可以将业务按模块(场景)划分,每个模块提供单独的配置文件,用于跟前端UI层级对应。以保证当项目整体层级需要改变时能够通过修改配置文件的方式进行统一修改。不同的场景应该提供不同的默认查找路径,提供默认的查抄方法,并另外提供其他模块的查找方法(这个方法应该尽量不对业务开发人员开放,避免程序健壮性受到影响)。不同的场景应该提供动态加载的功能,在场景切换时进行统一的加载和卸载,以保证合理的内存占用。
更多内容请查看总目录【Unity】Unity学习笔记目录整理