Unity UI框架

Posted 熊思宇

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity UI框架相关的知识,希望对你有一定的参考价值。

一、简介

最近在各大网站看了一下 Unity3d 的 UI 框架,各种 UI 框架已经有很多的版本了,各有千秋,有的功能虽然写的完善,但用起来太复杂,有的框架功能不完善,搞个课程就上架了,还有什么 MVC 框架,绕来绕去的看的头都大了,这些根本不想用。

于是我自己就写了一个 UI 框架,只有两个脚本,不用向 UI 预制体上挂载脚本,所有的组件访问都可以通过 UIManager 来控制,常用的几个方法:显示界面,关闭界面,查找子物体,就这么多。

二、UI 框架

下面的两个脚本是 UI 框架的核心部分,具体的用法在下面的章节有介绍

UIBase

using UnityEngine;

public class UIBase

    #region 字段
    
    /// <summary>
    /// Prefabs路径
    /// </summary>
    public string PrefabsPath  get; set; 
    /// <summary>
    /// UI面板的名字
    /// </summary>
    public string UIName  get; set; 
    /// <summary>
    /// 当前UI所在的场景名
    /// </summary>
    public string SceneName  get; set; 
    /// <summary>
    /// Type 的全名
    /// </summary>
    public string FullName  get; set; 
    /// <summary>
    /// 当前UI的游戏物体
    /// </summary>
    public GameObject UIGameObject  get; set; 


    #endregion

    /// <summary>
    /// 面板实例化时执行一次
    /// </summary>
    public virtual void Start()  

    /// <summary>
    /// 每帧执行
    /// </summary>
    public virtual void Update()  

    /// <summary>
    /// 当前UI面板销毁之前执行一次
    /// </summary>
    public virtual void Destroy()  


    /// <summary>
    /// 根据名称查找一个子对象
    /// </summary>
    /// <param name="name"></param>
    /// <returns></returns>
    public GameObject GetObject(string name)
    
        Transform[] trans = UIGameObject.GetComponentsInChildren<Transform>();
        foreach (var item in trans)
        
            if (item.name == name)
                return item.gameObject;
        

        Debug.LogError(string.Format("找不到名为 0 的子对象", name));
        return null;
    

    /// <summary>
    /// 根据名称获取一个子对象的组件
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    public T GetOrAddCommonent<T>(string name) where T : Component
    
        GameObject child = GetObject(name);
        if (child)
        
            if (child.GetComponent<T>() == null)
                child.AddComponent<T>();
            return child.GetComponent<T>();
        
        return null;
    


    protected UIBase()  

UIManager

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class UIManager : MonoBehaviour

    public static UIManager Instance;

    //存储场景中的UI信息
    private Dictionary<string, UIBase> UIDic = new Dictionary<string, UIBase>();

    //当前场景的 Canvas 游戏物体
    private Transform CanvasTransform = null;

    //当前字典中UI的个数
    public int UICount
    
        get  return UIDic.Count; 
    

    private void Awake()
    
        Instance = this;
    

    private void Start()
    

    

    private void Update()
    
        if (UIDic.Count > 0)
        
            foreach (var key in UIDic.Keys)
            
                if (UIDic[key] != null)
                    UIDic[key].Update();
            
        
    

    /// <summary>
    /// 显示面板
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public UIBase ShowUI<T>() where T : UIBase
    
        Type t = typeof(T);
        string fullName = t.FullName;

        if (UIDic.ContainsKey(fullName))
        
            Debug.Log("当前面板已经显示了,名字:" + fullName);
            return UIDic[fullName];
        

        GameObject canvasObj = GameObject.Find("Canvas");
        if (canvasObj == null)
        
            Debug.LogError("场景中没有Canvas组件,无法显示UI物体");
            return null;
        
        CanvasTransform = canvasObj.transform;

        UIBase uiBase = Activator.CreateInstance(t) as UIBase;
        if (string.IsNullOrEmpty(uiBase.PrefabsPath))
        
            Debug.LogError("Prefabs 路径不能为空");
            return null;
        

        GameObject prefabs = Resources.Load<GameObject>(uiBase.PrefabsPath);
        GameObject uiGameOjbect = GameObject.Instantiate(prefabs, CanvasTransform);
        uiGameOjbect.name = uiBase.PrefabsPath.Substring(uiBase.PrefabsPath.LastIndexOf('/') + 1);

        uiBase.UIName = uiGameOjbect.name;
        uiBase.SceneName = SceneManager.GetActiveScene().name;
        uiBase.UIGameObject = uiGameOjbect;
        uiBase.FullName = fullName;
        uiBase.Start();

        UIDic.Add(fullName, uiBase);
        return uiBase;
    

    /// <summary>
    /// 移除面板
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public void RemoveUI<T>()
    
        Type t = typeof(T);
        string fullName = t.FullName;

        if (UIDic.ContainsKey(fullName))
        
            UIBase uIBase = UIDic[fullName];
            uIBase.Destroy();

            GameObject.Destroy(uIBase.UIGameObject);
            UIDic.Remove(fullName);
            return;
        
        Debug.Log(string.Format("当前的UI物体未实例化,名字:0", fullName));
    

    /// <summary>
    /// 清除所有的UI物体
    /// </summary>
    public void ClearAllPanel()
    
        foreach (var key in UIDic.Keys)
        
            UIBase uIBase = UIDic[key];
            if (uIBase != null)
            
                uIBase.Destroy();
                GameObject.Destroy(uIBase.UIGameObject);
            
        

        UIDic.Clear();
    

    /// <summary>
    /// 找到指定的UI面板
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    public GameObject GetGameObject<T>(string name)
    
        Type t = typeof(T);
        string fullName = t.FullName;

        UIBase uIBase = null;
        if (!UIDic.TryGetValue(fullName, out uIBase))
        
            Debug.Log("没有找到对应的UI面板,名字:" + fullName);
            return null;
        

        return uIBase.GetObject(name);
    

    private UIManager()  

三、UI 框架的用法

我用了两个 场景来测试框架,start 和 main 场景

start 场景如下:

   

在 Start 场景 GameRoot 上挂上 StartSceneRoot 脚本

这个脚本就调用框架在场景中显示一个UI

using UnityEngine;

public class StartSceneRoot : MonoBehaviour 	
	void Start () 
		UIManager.Instance.ShowUI<Panel_MainUI>();
    

在 GameManager 游戏物体上有两个脚本,这个是要跟随着场景一起跳转的。

UIManager 的代码在上一节,下面是 GameManager 代码 

using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour 

    public static GameManager Instance;
    private static bool origional = true;

    private void Awake()
    
        if (origional)
        
            Instance = this as GameManager;
            origional = false;
            DontDestroyOnLoad(this.gameObject);
        
        else
        
            Destroy(this.gameObject);
        
    

    void Start () 
        SceneManager.sceneUnloaded += SceneManager_sceneUnloaded;
        SceneManager.sceneLoaded += SceneManager_sceneLoaded;
    

    private void SceneManager_sceneLoaded(Scene arg0, LoadSceneMode arg1)
    
        //Debug.Log("场景加载了,场景名:" + arg0.name);
    

    private void SceneManager_sceneUnloaded(Scene arg0)
    
        //Debug.Log("场景卸载了,场景名:" + arg0.name);

        //注意:切换场景要清除掉 UIManager 中保存的 UI 数据
        UIManager.Instance.ClearAllPanel();
    

    public void LoadScene(string sceneName)
    
        if (SceneManager.GetActiveScene().name != sceneName)
        
            SceneManager.LoadScene(sceneName);
        
    

    private GameManager()  

在 start 场景中挂载到游戏物体上的脚本就这三个:StartSceneRoot,GameManager,UIManager

下面就准备要显示的 UI 了,在这里,我做了三个 UI 界面,并做成预制体

界面 Panel_MainUI

界面 Panel_Setting

界面 Panel_Affiche

这三个预制体对应的也是三个脚本,但不用挂在游戏物体上,作用是UI的逻辑部分。

Panel_MainUI

using UnityEngine;
using UnityEngine.UI;

public class Panel_MainUI : UIBase

    public Panel_MainUI()
    
        PrefabsPath = "Prefabs/UI/Panel_MainUI";
    

    public override void Start()
    
        GetOrAddCommonent<Button>("Button_Setting").onClick.AddListener(() =>
        
            //显示设置面板
            UIManager.Instance.ShowUI<Panel_Setting>();
        );

        GetOrAddCommonent<Button>("Button_Task").onClick.AddListener(() =>
        
            //清除所有的面板
            //UIManager.Instance.ClearAllPanel();  

            //跳转到 main 场景
            GameManager.Instance.LoadScene("main");
        );

        GetOrAddCommonent<Button>("Button_Equipage").onClick.AddListener(() =>
        
            Debug.Log("UIManager 中 UI 面板的个数:" + UIManager.Instance.UICount);
        );
    

    public override void Update()
    
        //Debug.Log("我是 Panel_MainUI Update 方法");
    

    public override void Destroy()
    
        //Debug.Log("我是 Panel_MainUI Destroy 方法");
    

Panel_Setting

using UnityEngine;
using UnityEngine.UI;

public class Panel_Setting : UIBase

    public Panel_Setting()
    
        PrefabsPath = "Prefabs/UI/Panel_Setting";
    

    public override void Start()
    
        //Debug.Log("我是 Panel_Setting Start 方法");

        GetOrAddCommonent<Button>("Button_Close").onClick.AddListener(() =>
        
            //移除自己
            UIManager.Instance.RemoveUI<Panel_Setting>();
        );

        GetOrAddCommonent<Button>("Button_Test").onClick.AddListener(() =>
        
            //访问其他的面板的游戏物体 
            GameObject obj = UIManager.Instance.GetGameObject<Panel_MainUI>("Button_Map");
            if (obj != null)
                obj.transform.Find("Text").GetComponent<Text>().text = "Map";
        );

        GetOrAddCommonent<Button>("Button_Test1").onClick.AddListener(() =>
        
            Debug.Log("UIManager 中 UI 面板的个数:" + UIManager.Instance.UICount);
        );
    

    public override void Update()
    
        //Debug.Log("我是 Panel_Setting Update 方法");
    

    public override void Destroy()
    
        //Debug.Log("我是 Panel_Setting Destroy 方法");
    

Panel_Affiche

using UnityEngine;
using UnityEngine.UI;

public class Panel_Affiche : UIBase

    public Panel_Affiche()
    
        PrefabsPath = "Prefabs/UI/Panel_Affiche";
    

    public override void Start()
    
        GetOrAddCommonent<Button>("Button_Close").onClick.AddListener(() =>
        
            GameManager.Instance.LoadScene("start");
        );
    

    public override void Update()
    
        //Debug.Log("我是 Panel_Affiche Update 方法");
    

    public override void Destroy()
    
        //Debug.Log("我是 Panel_Affiche Destroy 方法");
    

main 场景只挂了一个脚本

MainSceneRoot 脚本同样也是只是用来显示UI用的

using UnityEngine;

public class MainSceneRoot : MonoBehaviour 
	void Start () 
		UIManager.Instance.ShowUI<Panel_Affiche>();
	

运行后就能看到,显示了 Panel_MainUI 的UI界面,点击设置,就会打开设置的界面,点击任务按钮,就会跳转到 main 场景(这里只是测试)

跳转到 main 场景后,点击关闭按钮,又会返回到 start 场景。

演示就这些了,写这个框架我也就用了几个小时而已,当然还有待继续完善和改进的,另外,我用的 Unity版本是 Unity 5.6.7f1 ,C# 的一些高级语法用不了,不然可以写的更优雅一些。

源码:点击跳转

结束

如果这个帖子对你有所帮助,欢迎 关注 + 点赞 + 留言

end

Unity | Unity中UI框架的实现与使用

0 前言

作为初学者常用的UI设计方式以及与UI与游戏对象交互的方式是使用简单的对象拖拽,但是这样的方式耦合性很高,而且对于协作项目来说不太友好,所以使用一个单一的UI框架管理Resources中的UIPanel预制体,在需要时进行创建和销毁是很有必要的,其一可以降低UI与游戏程序之间的耦合性,提高UI的复用能力,其二可以优化游戏性能,在必要的时候才创建所需的UI组件,减少在开始时大量创建UI的性能开销。

1 程序结构

UIManager

  • UIManager.cs : Class
  • BasePanel.cs : Class
  • UIPanelInfo.cs : Class
  • UIPanelType.cs : Enum
  • UIPanel.json : Json

2 工具类:UIPanelInfo.cs

该类用于对Json文件进行反序列化,将Json文本中的路径和类型字段转换为对应的对象,利用了C#中对Json进行序列化操作的原生接口ISerializationCallbackReceiver的回调方法处理了Json对象。

using System;
using UnityEngine;

// 反序列化json文本,读取其中存储的Panel路径
[Serializable]
public class UIPanelInfo : ISerializationCallbackReceiver

    [NonSerialized] 
    public UIPanelType panelType;

    public string panelTypeString;
    public string path;

    //序列化
    public void OnBeforeSerialize()
    
        
    

    // 反序列化,从文本到对象
    public void OnAfterDeserialize()
    
        var type = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), panelTypeString);
        panelType = type;
    

3 PanelType枚举类以及BasePanel类

PanelType类用于为单个Panel赋予唯一Key用来生成和调用Panel。

public enum UIPanelType

    PanelType,
    ...

BasePanel类是对所有Panel的声明周期进行了定义的基类,在其中我对GetOverUI和FindUIElem方法进行了封装,用来获得所需要的对象,其余方法是对Panel生命周期的虚函数声明,包括OnEnter(进入时)、OnPause(暂停时)、OnResume(继续时)、OnExit(退出时)、OnClose(关闭时)这五个生命周期,在继承其的基类中,可以override这几个虚函数,用来对不同生命周期中的Panel进行操作,同时强推一下CanvasGroup这个组件,对于Panel的效果实现有很大帮助。

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class BasePanel : MonoBehaviour

    private CanvasGroup canvasGroup;

    public CanvasGroup CanvasGroupValue
    
        get
        
            return canvasGroup;
        
        set
        
            canvasGroup = value;
        
    

    private void Awake()
    
        canvasGroup = GetComponent<CanvasGroup>();
    
    
    /// <summary>
    /// 显示UI时调用
    /// </summary>
    public virtual void OnEnter()
    
        canvasGroup.alpha = 1;
    

    public virtual void OnPause()
    
        canvasGroup.blocksRaycasts = false;
    

    public virtual void OnResume()
    
        canvasGroup.blocksRaycasts = true;
    

    public virtual void OnExit()
    
        canvasGroup.alpha = 0;
    

    public virtual void OnClose()
    
        UIManager.Instance.PopPanel();
    
    
    protected GameObject GetOverUI(GameObject canvas)
    
        PointerEventData pointerEventData = new PointerEventData(EventSystem.current);
        pointerEventData.position = Input.mousePosition;
        GraphicRaycaster gr = canvas.GetComponent<GraphicRaycaster>();
        List<RaycastResult> results = new List<RaycastResult>();
        gr.Raycast(pointerEventData, results);
        if (results.Count != 0)
        
            return results[0].gameObject;
        
        return null;
    
    
    protected GameObject FindUIElement(string elementName)
    
        GameObject returnGameObject = null;
        foreach (var child in gameObject.GetComponentsInChildren<Transform>(true))
        
            if (child.name.Equals(elementName))
            
                returnGameObject = child.gameObject;
            
        
        return returnGameObject;
    

4 UIManager类

UIManager类是该UI框架的主要逻辑类,这个类的数据结构由存放Json中path字段的字典、每个BasePanel组件的字典、以及一个存放了Panel对象的栈组成,同时因为UIManager的特性,在类设计上使用了单例模式。
构成该类的方法主要是对Panel栈的操作方法,包括PushPanel(入栈)和PopPanel(出栈)两个主要方法。
最后为该类设计了一个内部类用来获取Json中的path和panelType,这个类使用了JsonUtility来获得Json对象的信息。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine.Video;

public class UIManager : MonoBehaviour

    private static UIManager _instance;

    public static UIManager Instance
    
        get
        
            if (_instance == null)
            
                _instance = new UIManager();
            

            return _instance;
        
    

    private Transform _canvasTransform;

    private Transform CanvasTransform
    
        get
        
            if (_canvasTransform == null)
            
                _canvasTransform = GameObject.Find("MainCanvas").transform;
            

            return _canvasTransform;
        
    

    private Dictionary<UIPanelType, string> panelPathDictionary;
    private Dictionary<UIPanelType, BasePanel> panelDictionary;

    private Stack<BasePanel> panelStack;

    private UIManager()
    
        ParseUIPanelTypeJson();
    

    public void PushPanel(UIPanelType panelType)
    
        if (panelStack == null)
        
            panelStack = new Stack<BasePanel>();
        

        if (panelStack.Count > 0)
        
            BasePanel topPanel = panelStack.Peek();
            topPanel.OnPause();
        

        BasePanel panel = GetPanel(panelType);
        panel.OnEnter();
        panelStack.Push(panel);
    

    public void PopPanel()
    
        if (panelStack == null)
        
            panelStack = new Stack<BasePanel>();
        

        if (panelStack.Count <= 0)
        
            return;
        

        BasePanel basePanel = panelStack.Pop();
        basePanel.OnExit();

        if (panelStack.Count <= 0) return;
        BasePanel topPanel = panelStack.Peek();
        topPanel.OnResume();
    

    private BasePanel GetPanel(UIPanelType panelType)
    
        if (panelDictionary == null)
        
            panelDictionary = new Dictionary<UIPanelType, BasePanel>();
        

        panelDictionary.TryGetValue(panelType, out var panel);

        if (panel == null)
        
            panelPathDictionary.TryGetValue(panelType, out var path);
            
            GameObject initPanel = Instantiate(Resources.Load(path)) as GameObject;
            initPanel.transform.SetParent(CanvasTransform, false);
            
            panelDictionary.Add(panelType, initPanel.GetComponent<BasePanel>());
            return initPanel.GetComponent<BasePanel>();
        
        else
        
            return panel;
        
    

    [SerializeField]
    class UIPanelTypeJson
    
        public List<UIPanelInfo> infoList;
    

    private void ParseUIPanelTypeJson()
    
        panelPathDictionary = new Dictionary<UIPanelType, string>();

        TextAsset textAsset = Resources.Load<TextAsset>("Json/UIPanels");

        UIPanelTypeJson jsObject = JsonUtility.FromJson<UIPanelTypeJson>(textAsset.text);

        foreach (UIPanelInfo info in jsObject.infoList)
        
            panelPathDictionary.Add(info.panelType, info.path);
        
    

5 UIPanelJson文件

用来存放Panel预制体路径和对应的panelType


  "infoList": [
    
      "panelTypeString" : "MainMenuPanel",
      "path" : "UIPanel/MainMenuPanel"
    
  ]

5 使用方法

  1. 在json文件中写入对应的Panel预制体路径和panelType

  "infoList": [
    
      "panelTypeString" : "PanelType",
      "path" : "UIPanel/XXXPanel"
    
  ]

  1. 在PanelType枚举类中写入Json文件中创建的PanelType
public enum UIPanelType

    PanelType

  1. 在方法中调用PushPanel方法创建panelTyep对应的Panel对象
UIManager.Instance.PushPanel(panelType);
  1. 修改XXXPanel类中的生命周期方法来实现面板效果(示例:一个简单的UI界面类)
using System;
using System.Collections;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;

public class UserInterfacePanel : BasePanel

    private int Health;

    private int sub = 0;

    private int ableBullet = 0;

    void Awake()
    
        EventCenter.AddListener<int>(EventType.BulletRefresh, RefreshBullet);
        EventCenter.AddListener<int>(EventType.HealthRefresh, RefreshHealth);
    

    private void Start()
    
        StartCoroutine(CheckTheNumericValue());
    

    void RefreshHealth(int health)
    
        var text = transform.Find("Health").transform.Find("OnTimeHealth").GetComponent<Text>();
        text.text = health.ToString();
    

    void RefreshBullet(int bulletContain)
    
        ableBullet = bulletContain;
    

    public override void OnEnter()
    
        UIManager.Instance.PushPanel(UIPanelType.TipPanel);
        UIManager.Instance.PushPanel(UIPanelType.PackagePanel);
    

    IEnumerator CheckTheNumericValue()
    
        while (true)
        
            yield return new WaitForSeconds(0.1f);
            var text = FindUIElement("BulletContain").GetComponent<Text>();
            if (Package.Instance.GetItem(ItemType._9mmAmmo) != null)
            
                text.text = $"ableBullet / Package.Instance.GetItem(ItemType._9mmAmmo).count";
            
        
    

6 结篇

框架对程序结构以及性能的优化是十分强大的,可以多用。

以上是关于Unity UI框架的主要内容,如果未能解决你的问题,请参考以下文章

Unity UI框架

Unity UI框架思路与实现

基于Unity的编辑器开发: GUI框架技术

Unity UI框架(一 窗口层级管理)

unity客户端基本框架(转载)

Unity引擎UI模块知识Tree