Unity学习日记3 #UI框架

制作一个Unity的UI框架

1.UI拼接并制成预制体(😊轻松第一步)

  • 将UI拼接好后全部制成预制体
  • 一个界面一个Panel
    请添加图片描述

2.使用读取Json的方式获取预制体路径(🤗小小学一手Json)

通过读取Json文件反序列化读取所有预制体路径

1.编写Json配置文件

  • JSON是一种取代XML的数据结构,与xml相比,它更小巧,但描述能力却不差。JSON就是一串字符串,只不过元素会使用特定的符号标注:

    • {} 双括号表示对象;
    • [] 中括号表示数组;
    • "" 双引号内是属性或值;
    • : 冒号表示后者是前者的值(这个值可以是字符串、数字、也可以是另一个数组或对象)

    所以 {"name": "Tom"} 可以理解为是一个包含name为Tom的对象,而 [{"name: "Tom"}, {"name": "Jerry"}] 就表示包含两个对象的数组。

{
    "infoList": [
        {
            "panelTypeString": "BagPanel",
            "path": "UIPrefab/BagPanel"
        },
        {
            "panelTypeString": "MainMenuPanel",
            "path": "UIPrefab/MainMenuPanel"
        },
        {
            "panelTypeString": "SkillPanel",
            "path": "UIPrefab/SkillPanel"
        },
        {
            "panelTypeString": "SystemPanel",
            "path": "UIPrefab/SystemPanel"
        },
        {
            "panelTypeString": "TaskPanel",
            "path": "UIPrefab/TaskPanel"
        },
        {
            "panelTypeString": "ShopPanel",
            "path": "UIPrefab/ShopPanel"
        },
        {
            "panelTypeString": "ItemMessagePanel",
            "path": "UIPrefab/ItemMessagePanel"
        }
    ]
}

2.不确定Json格式正不正确?

  • 搜索Json格式化,在线检验格式是否正确

  • 地址 Josn.cn

3.使用类创建对象读取配置文件信息

  • 序列化与反序列化

    • 序列化:将对象转换成二进制数据

    • 反序列化:将二进制的数据转换为对象

  • 标签[Serializable]表示这个类是可序列化的,由C#提供

  • 接口ISerializationCallbackReceiver

  • JsonUtility.FromJson<T>:将Json信息转化为对象

  • JsonUtility.ToJson<T>:将对象转化为Json

  • OnAfterDeserialize() 反序列化之后被Unity引擎调用

  • OnBeforeSerialize() 序列化之前被Unity引擎调用

  • 🚀UIPannelTypeJson

    • infoList<UIPanelInfo>存储UIpanel的队列
      • 相当于上面Json中的"InfoList": […省略]
  • 🚀UIPanelType

    • UIpanel类型枚举
  • 🚀UIPanelInfo

    • infoList<UIPanelInfo>中的单个元素

      • 相当于上面Json中的"InfoList"数组里面的单个元素

      • {
            "panelTypeString":"BagPanel",
            "path": "UIPrefab/BagPanel"
        }
        
using System.Collections.Generic;
using System;
using UnityEngine;

/*
 * 序列化:将对象转换成二进制数据
 * 反序列化:将二进制的数据转换为对象
 * 标签[Serializable]
 * 
 * JsonUtility.FromJson<T>:将Json信息转化为对象
 * JsonUtility.ToJson<T>:将对象转化为Json
 * OnAfterDeserialize() 反序列化之后被Unity引擎调用
 * OnBeforeSerialize() 序列化之前被Unity引擎调用
*/

public enum UIPanelType
{
    BagPanel,
    MainMenuPanel,
    SkillPanel,
    SystemPanel,
    TaskPanel
}

[Serializable]
public class UIPanelInfo:ISerializationCallbackReceiver
{
    public string panelTypeString;
    public string path;
    public UIPanelType uIPanelType;
    public void OnAfterDeserialize()
    {
        uIPanelType = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), panelTypeString);
    }

    public void OnBeforeSerialize()
    {

    }
}

[Serializable]
class UIPannelTypeJson
{
    public List<UIPanelInfo> infoList;
}

  • 🚀UIManager的作用
    • 存储通过Json反序列化读取到的路径信息
using System.Collections.Generic;
using UnityEngine;

public class UIManager
{
    private static UIManager instance;
    public static UIManager Instance
    {
        get 
        {
            if (instance == null)
            {
                instance = new UIManager();
            }
            return instance;
        }
    }
    Dictionary<UIPanelType, string> panelPathDic= new Dictionary<UIPanelType, string>();
    private UIManager() 
    {
        ParseJson();
    }
    void ParseJson()
    {
        TextAsset textAsset = Resources.Load<TextAsset>("Config/UIPanelType");
        UIPannelTypeJson uIPannelTypeJson = JsonUtility.FromJson<UIPannelTypeJson>(textAsset.text);
        foreach (UIPanelInfo item in uIPannelTypeJson.infoList)
        {
            panelPathDic.Add(item.uIPanelType,item.path);
        }
    }
}

3.实例化UI预制体并存储(👌渐入佳境)

  • 首先思考,玩家本次打开游戏不点击UI界面我们真的需要加载吗?肯定是不加载拉!

  • 那我怎么知道这个UI有没有被实例化过?使用Dictionary(基于哈希表)以键值对的形式存储已经加载UIPanel

  • 那么这个Dictionary的键值对分别用什么?键我们使用枚举!(不使用字符串,因为可能打错字,枚举只需要选择,非常方便)

    • 前面我们已经用代码实现了字符串转枚举,接口由c#提供,看下面代码将panelTypeString成员转化成一个UIPanelType类型的临时对象,赋值给uIPanelType成员。
    //这是第2步的代码,并不是增加的代码
    public void OnAfterDeserialize()
        {
            uIPanelType = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), panelTypeString);
        }
    
  • 那么如何选择值类型?直接实例化存储GameObject?不方便,每次要拿到每个UIPanel身上的UI控制脚本都需要GetComponent。显而易见,虽然每个UIPanel界面的逻辑不同,但是每个UIPanel的UI控制脚本都都肯定相同的特征,我们直接继承同一个基类BasePanel,在Dictionary字典中,我们将这个基类作为我们存储的值。新增字典Dictionary<UIPanelType,BasePanel> panelDic

    以及类public class BasePanel : MonoBehaviour{}

  • 此时我们的BasePanel脚本还有继承它的子类都没有挂载到相应的UIPanel上,我们应该什么挂上?直接在制作预制体的时候拖入?不安全,不方便。我们直接在第一次创建的实例的时候,通过 Dictionary<UIPanelType, string> panelPathDic字典中元素的成员uIPanelType确定Panel类型。然后的话,如果是菜鸟的我肯定是switch case 或者 if else,来判断给UIPanel添加什么脚本。但是神😇教了我反射!

  • 反射即程序可以在运行时获得程序或者程序集中每一个类型(包括类、结构、委托、接口、枚举)的成员和成员信息,访问、检测、修改它本身的状态或行为的一种能力。(😭快点去学)

  • 🐎🐎再也不用担心我switch case了

  • void AddScriptComponent(UIPanelType uIPanelType, GameObject go)
    {
        string scriptName = System.Enum.GetName(uIPanelType.GetType(), uIPanelType);
        Type scriptType = Type.GetType(scriptName);//注意这个scriptName一定要和你的脚本名字一样,我是将枚举的名字弄成和脚本对应,比较好拿
        if (scriptType == null)//防止脚本还没写全,只写了一部分拿不到,做完即删
        {
        	return;
        }
    
        if (!go.GetComponent(scriptType))//如果没有这个组件
        {
        	go.AddComponent(scriptType);//添加这个组件
        }
    }
    
    //用来添加和获取脚本
    public T GetAndAddComponent<T>(GameObject go) where T : Component
    {
        if (!go.GetComponent<T>())
        {
            go.AddComponent<T>();
        }
        return go.GetComponent<T>();
    }
    
  • 然后我们需要再给所以的UIPanel添加一个CanvasGroup组件,它的作用是可以控制本物体下的所有UI物体能否接收射线(不能接收则无法响应),是否可以用,这样不要用这个UIPanel的时候就可以直接禁用这些UI了

  • GetAndAddComponent<CanvasGroup>(instancePanel);

  • 获取UIPanel的代码如下

    • 如果实例化过就直接从字典拿,如果没有就先实例化再存储到字典中
    Dictionary<UIPanelType,BasePanel> panelDic = new Dictionary<UIPanelType,BasePanel>();
    
    private BasePanel GetPanel(UIPanelType uIPanelType)
    {
        BasePanel panel;
        panelDic.TryGetValue(uIPanelType,out panel);
        if (panel == null)
        {
            string path;
            panelPathDic.TryGetValue(uIPanelType, out path);
            if (path != null)
            {
                GameObject instancePanel = GameObject.Instantiate(Resources.Load<GameObject>(path));
                instancePanel.transform.SetParent(CanvasTrans,false);
                AddScriptComponent(uIPanelType, instancePanel);
                panelDic.Add(uIPanelType, GetAndAddComponent<BasePanel>(instancePanel));
                GetAndAddComponent<CanvasGroup>(instancePanel);
                return instancePanel.GetComponent<BasePanel>();
            }
            else
            {
                Debug.LogError(uIPanelType + "类型界面对应路径不存在,请查正配置文件");
                return null;
            }
        }
        else//界面被实例化过
        {
            return panel;
        }
    }
    
  • 再思考UIPanel,有几种状态

    • 没有被玩家点开,

      初始状态,如果玩家这次游戏不点这个UIpanel,那么它这次就不会被加载

    • 被玩家点开在最上层,

      我们应该要让UIpanel可见,并可以响应

    • 被另一个UIPanel遮盖

      我们应该关闭UIPanel的响应,让它不再接收玩家的指令,只有最上层的UIPanel应该接收玩家的指令

    • 被玩家关闭

      我们应该要让UIpanel不可见,并不可响应

  • 我们需要转换BasePanel四个状态,所以在基类中拥有四个虚函数(为了后续增加功能重写)

    • using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      using UnityEngine.UI;
      
      public class BasePanel : MonoBehaviour
      {
          public virtual void OnEnter()//进入,玩家初次点开Panel
          {
          }
          public virtual void OnResume()//恢复,即上层Panel被玩家关闭,恢复到最上层
          {
          }
          public virtual void OnPause()//停止,即Panel被新的一层Panel覆盖了,如在背包界面点开了道具详细信息界面
          { 
          }
          public virtual void OnExit()//离开,被玩家关闭
          {
          }
      }
      
  • 那么问题来了,我们怎么知道哪个UI在顶层?先观察特征,先被点开的在最下层,最后被点开的在最上层,需要一层一层被关闭,不能关闭下层UIpanel,那么答案显而易见😋我们使用Stack栈,定义Stack<BasePanel> panelStack,来存储被玩家点开的Panel,首先主要UIPanel在栈底永不出栈,我们在游戏场景加载时就入栈,玩家每点开一个UIPanel,就调用接口入栈,此时如果栈非空,那么栈顶的UIPanel应该调用OnPause(),即将进入的UIPanel应该调用OnEnter。玩家点击关闭一个UIPanel,应该出栈,此时栈顶元素先调用OnExit再出栈,新的栈顶元素应该调用OnResume()恢复。

  • UIPanel栈(UIManager中)代码如下

    Stack<BasePanel> panelStack = new Stack<BasePanel>();
    
    public void PushPanel(UIPanelType uIPanelType)
     {
         if (panelStack.Count > 0)
         {
             BasePanel topPanel = panelStack.Peek();
             topPanel.OnPause();
         }
         BasePanel panel = GetPanel(uIPanelType);
         panel.OnEnter();
         panelStack.Push(panel);
     }
    
    public void PopPanel()
    {
        if (panelStack.Count <= 0)
        {
            return;
        }
        BasePanel topPanel = panelStack.Pop();
        topPanel.OnExit();
        if (panelStack.Count <= 0)
        {
            return;
        }
        BasePanel newTopPanel = panelStack.Peek();
        newTopPanel.OnResume();
    }
    
  • 完善BasePanel最基本的功能

    using UnityEngine;
    using UnityEngine.UI;
    
    public class BasePanel : MonoBehaviour
    {
        protected CanvasGroup canvasGroup;
    
        public virtual void OnEnter()
        {
            if (canvasGroup == null)
            {
                canvasGroup = GetComponent<CanvasGroup>();
            }
            canvasGroup.blocksRaycasts = true;
            canvasGroup.alpha = 1;
            
        }
        public virtual void OnResume()
        {
            canvasGroup.blocksRaycasts = true;
        }
        public virtual void OnPause()
        { 
            canvasGroup.blocksRaycasts = false;
        }
        public virtual void OnExit()
        {
            canvasGroup.blocksRaycasts = false;
            canvasGroup.alpha = 0;
        }
        protected virtual void OnClickCloseBtn()
        {
            UIManager.Instance.PopPanel();
        }
    }
    
  • 🚀UIManager补全代码

    using System;
    using System.Collections.Generic;
    using Unity.VisualScripting;
    using UnityEngine;
    
    public class UIManager
    {
        private static UIManager instance;
        public static UIManager Instance
        {
            get 
            {
                if (instance == null)
                {
                    instance = new UIManager();
                }
                return instance;
            }
        }
        Dictionary<UIPanelType, string> panelPathDic = new Dictionary<UIPanelType, string>();
        Dictionary<UIPanelType,BasePanel> panelDic = new Dictionary<UIPanelType,BasePanel>();
        Stack<BasePanel> panelStack = new Stack<BasePanel>();
        private Transform canvasTrans;
        public Transform CanvasTrans
        {
            get 
            {
                if (canvasTrans == null)
                {
                    canvasTrans = GameObject.Instantiate(Resources.Load<GameObject>("UIPrefab/Canvas")).transform;
                }
                return canvasTrans;
            }
        }
        private UIManager() 
        {
            ParseJson();
        }
        void ParseJson()
        {
            TextAsset textAsset = Resources.Load<TextAsset>("Config/UIPanelType");
            UIPannelTypeJson uIPannelTypeJson = JsonUtility.FromJson<UIPannelTypeJson>(textAsset.text);
            foreach (UIPanelInfo item in uIPannelTypeJson.infoList)
            {
                panelPathDic.Add(item.uIPanelType,item.path);
            }
        }
    
        private BasePanel GetPanel(UIPanelType uIPanelType)
        {
            BasePanel panel;
            panelDic.TryGetValue(uIPanelType,out panel);
            if (panel == null)
            {
                string path;
                panelPathDic.TryGetValue(uIPanelType, out path);
                if (path != null)
                {
                    GameObject instancePanel = GameObject.Instantiate(Resources.Load<GameObject>(path));
                    instancePanel.transform.SetParent(CanvasTrans,false);
                    AddScriptComponent(uIPanelType, instancePanel);
                    panelDic.Add(uIPanelType, GetAndAddComponent<BasePanel>(instancePanel));
                    GetAndAddComponent<CanvasGroup>(instancePanel);
                    return instancePanel.GetComponent<BasePanel>();
                }
                else
                {
                    Debug.LogError(uIPanelType + "类型界面对应路径不存在,请查正配置文件");
                    return null;
                }
            }
            else//界面被实例化过
            {
                return panel;
            }
        }
    
        void AddScriptComponent(UIPanelType uIPanelType, GameObject go)
        {
            string scriptName = System.Enum.GetName(uIPanelType.GetType(), uIPanelType);
            Type scriptType = Type.GetType(scriptName);
            if (scriptType == null)//防止脚本还没写全,只写了一部分拿不到,做完即删
            {
                return;
            }
    
            if (!go.GetComponent(scriptType))
            {
                go.AddComponent(scriptType);
            }
        }
    
        public T GetAndAddComponent<T>(GameObject go) where T : Component
        {
            if (!go.GetComponent<T>())
            {
                go.AddComponent<T>();
            }
            return go.GetComponent<T>();
        }
    
        public void PushPanel(UIPanelType uIPanelType)
        {
            if (panelStack.Count > 0)
            {
                BasePanel topPanel = panelStack.Peek();
                topPanel.OnPause();
            }
            BasePanel panel = GetPanel(uIPanelType);
            panel.OnEnter();
            panelStack.Push(panel);
        }
    
        public void PopPanel()
        {
            if (panelStack.Count <= 0)
            {
                return;
            }
            BasePanel topPanel = panelStack.Pop();
            topPanel.OnExit();
            if (panelStack.Count <= 0)
            {
                return;
            }
            BasePanel newTopPanel = panelStack.Peek();
            newTopPanel.OnResume();
        }
    }
    
    

4.示例以及效果展示😍😍

  • 示例脚本 GameRoot(游戏UI驱动器)->MainMenuPanel -> BagPanel-> ItemMessagePanel

    using UnityEngine;
    public class GameRoot : MonoBehaviour
    {
        void Start()
        {
            UIManager uiManager = UIManager.Instance;
            uiManager.PushPanel(UIPanelType.MainMenuPanel);
        }
    }
    
    using UnityEngine;
    using UnityEngine.UI;
    
    public class MainMenuPanel : BasePanel
    {
        private Button taskBtn;
        private Button shopBtn;
        private Button bagBtn;
        private Button skillBtn;
        private Button systemBtn;
        void Start()
        {
            taskBtn = transform.Find("Function/Task").GetComponent<Button>();
            shopBtn = transform.Find("Function/Shop").GetComponent<Button>();
            skillBtn = transform.Find("Function/Skill").GetComponent<Button>();
            bagBtn = transform.Find("Function/Bag").GetComponent<Button>();
            systemBtn = transform.Find("Function/System").GetComponent<Button>();
    
            taskBtn.onClick.AddListener(delegate () {
                BtnOnClick(UIPanelType.TaskPanel);
            });
            shopBtn.onClick.AddListener(delegate () {
                BtnOnClick(UIPanelType.ShopPanel);
            });
            skillBtn.onClick.AddListener(delegate () {
                BtnOnClick(UIPanelType.SkillPanel);
            });
            bagBtn.onClick.AddListener(delegate () {
                BtnOnClick(UIPanelType.BagPanel);
            });
            systemBtn.onClick.AddListener(delegate () {
                BtnOnClick(UIPanelType.SystemPanel);
            });
        }
    
        void BtnOnClick(UIPanelType uIPanelType)
        {
            UIManager.Instance.PushPanel(uIPanelType);
        }
        // Update is called once per frame
        public override void OnEnter()
        {
            base.OnEnter();
            canvasGroup.alpha = 1.0f;
            canvasGroup.blocksRaycasts = true;
        }
    
        public override void OnPause()
        {
            canvasGroup.blocksRaycasts = false;
        }
    }
    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class BagPanel : BasePanel
    {
        protected Button closeBtn;
        protected Button Item;
        // Start is called before the first frame update
        void Start()
        {
            closeBtn = transform.Find("CloseBtn").GetComponent<Button>();
            closeBtn.onClick.AddListener(OnClickCloseBtn);
            Item = transform.Find("Slot/Item").GetComponent<Button>();
            Item.onClick.AddListener(() =>
            {
                UIManager.Instance.PushPanel(UIPanelType.ItemMessagePanel);
            });
        }
    }
    
    using UnityEngine;
    using UnityEngine.UI;
    
    public class ItemMessagePanel : BasePanel
    {
        protected Button closeBtn;
        void Start()
        {
            closeBtn = transform.Find("CloseBtn").GetComponent<Button>();
            closeBtn.onClick.AddListener(OnClickCloseBtn);
        }
    }
    
  • 初始场景

  • 程序运行后,可以点击生产UI界面

    请添加图片描述

请添加图片描述

5.DoTween插件

  • 这个插件可以让我们的UI看起来更加炫酷
  • 在资源商店下载并安装DOTween:DOTween
    在PackageManager导入到要使用的项目中。
    安装完成后点击 Setup DOTween 会自动根据unity的版本导入/重新导入内部的一些文件,激活或者停用一些模块。
    如果不小心关闭或者关闭了想再次打开,你可以在unity的工具栏的Tools/Demigiant/DOTween Utility Panel 打开该面板。
  • 简单示例如下,在上述UI的OnEnter中添加如下代码
public override void OnEnter()
{
    base.OnEnter();
    Vector3 tempPos = transform.localPosition;
    tempPos.x = 3000;
    transform.localPosition = tempPos;
    transform.DOLocalMoveX(0, 1.0f);//一秒钟移动到指定位置
}
  • 效果是UI从屏幕右边滑过来
  • DoTween还有很多功能(学习中…还需努力🚑)

请添加图片描述
请添加图片描述

  • 27
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity UI框架是一种用于创建用户界面(UI)的工具集和组件集合。它提供了丰富的UI元素和功能,可以帮助开发者轻松地构建交互性和可视化的界面。 Unity UI框架可以通过Unity Asset Store进行下载。打开Unity编辑器,点击"Window"菜单,选择"Asset Store"选项,会打开Asset Store窗口。在搜索栏中输入"Unity UI框架",可以找到很多可用的UI框架。 在选择和下载UI框架之前,开发者应该了解项目的需求并进行评估。可以根据项目的规模、复杂度和用户需求来选择合适的UI框架。一些常用的UI框架包括:"TextMeshPro"、"DOTween" 和 "UGUI"等。开发者可以根据自己的需要选择适合的框架。 下载UI框架后,可以将其导入到Unity项目中。在Asset Store或者Unity Package Manager中选择合适的UI框架,点击"Download" 或者 "Import"按钮进行安装。安装完成后,可以在Unity编辑器的"Assets"文件夹中找到导入的UI框架。 使用UI框架时,可以在Unity编辑器中创建UI元素,如按钮、文本、滑动条等,并对其进行布局和样式设置。可以通过脚本代码来实现交互性功能,如按钮的点击事件、输入框的文本处理等。UI框架还提供了很多可定制的功能和效果,如动画、过渡和绘制等,可以增强用户界面的视觉效果和交互体验。 总之,Unity UI框架是一个方便使用的工具集,可以帮助开发者快速构建用户界面。通过下载合适的UI框架,开发者可以轻松地创建、布局和管理UI元素,提供良好的用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值