【Unity】Virtual reality VR UI framework construction


introduce

This article makes a simple UI framework for VRTK. Since the UI of VR games is relatively complex, ordinary UGUI cannot meet the requirements, so we customize a UI framework that is more suitable for VR to facilitate development and management.

need

  1. Unified management of the UI canvas (Canvas) (recording, providing display and hiding functions).
  2. UI event management.

Class Diagram

insert image description here

Core framework classes:

  1. UI window class UIWindow: The base class of all UI windows, which can represent all windows (conceptual integration, manage classes in a hierarchical manner), and define the common behavior of all windows (visible and hidden).
  2. UI management class UIManager: manage (record, disable, search) windows, define the common behavior of all windows (obtain listeners).
  3. UI event listener class UIEventListener: Provides all current UI events (with event parameter classes).

Function control class:

  1. Game Controller GameController: Responsible for handling the game process, such as displaying the main window before the game starts.

Tools:

  1. Singleton functional class MonoSingleton: Provides a singleton pattern, which can be implemented after inheriting this class.
  2. Transform component helper class TransformHelper: Provide some help methods for transform components.

Application class:

  1. UI main window class UIMainWindow: attached to the main window, responsible for processing the logic of the main window.

UI structure

insert image description here
insert image description here

  • root object UIManager
    • Window xxxWindow : UIWindow
      • interactive elements
    • Window xxxWindow : UIWindow
      • interactive elements

class development

Core Framework Classes

  1. UI window class UIWindow: The base class of all UI windows, which can represent all windows (conceptual integration, manage classes in a hierarchical manner), and define the common behavior of all windows (visible and hidden).
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];
        }
    }
}
  1. UI management class UIManager: manage (record, disable, search) windows, define the common behavior of all windows (obtain listeners).
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;
        }
    }
}
  1. UI event listener class UIEventListener: Provides all current UI events (with event parameter classes).
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);
        }
    }
}

Function control class

  1. Game Controller GameController: Responsible for handling the game process, such as displaying the main window before the game starts.
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);
            // 创建敌人

        }

        // 游戏结束
        // 游戏暂停
    }
}

Tools

  1. Singleton functional class MonoSingleton: Provides a singleton pattern, which can be implemented after inheriting this class.
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()
        {
    
    

        }
    }
}
  1. Transform component helper class TransformHelper: Provide some help methods for transform components.
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;
        }
    }
}

application class

  1. UI main window class UIMainWindow: attached to the main window, responsible for processing the logic of the main window.
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();
        }
    }
}

Instructions

  1. Define UIXXXWindow class, inherited from UIWindow, responsible for processing the window logic. Get the UI elements that need to be interacted with through GetUIEventListener.
  2. Members of the window are accessed through UIManager.Instance.GetWindow<window type>().method().

further improvement

In fact, for a large-scale project, the hierarchical structure of a single page may be very complex and huge, and the query complexity should be reduced as much as possible. The business can be divided into modules (scenarios), and each module provides a separate configuration file to correspond to the front-end UI level. To ensure that when the overall level of the project needs to be changed, it can be modified uniformly by modifying the configuration file. Different scenarios should provide different default search paths, provide default search methods, and provide search methods for other modules in addition (this method should not be open to business developers as much as possible to avoid affecting the robustness of the program). Different scenes should provide the function of dynamic loading, and perform unified loading and unloading when switching scenes to ensure reasonable memory usage.


For more information, please check the general catalog [Unity] Unity study notes catalog arrangement

Guess you like

Origin blog.csdn.net/xiaoyaoACi/article/details/121272882