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); }
- 前面我们已经用代码实现了字符串转枚举,接口由c#提供,看下面代码将
-
那么如何选择值类型?直接实例化存储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还有很多功能(学习中…还需努力🚑)