前言
提醒:为了能够将知识点学得更加透彻、记得更加牢固 我会通过教学讲解的方式把知识写下来 因为在过程中会让人从学生变成老师 这个过程会挖掘出新的知识和观点 是一个自我思维切换而达成的知识深度挖掘和提升的过程 如果能帮助到大家那就最好 如果有讲错的地方还请多多指教!我只是一只菜鸡 感谢理解!
为什么要使用UI框架
游戏中存在非常多的面板 例如:背包面板、商店面板、设置面板、
这些面板之间会需要频繁的调用和切换,例如:背包面板中存在各种物品,我们鼠标放到物品上会出现物品信息的提示框,还有设置面板需要按返回键才可以切换回主菜单面板,那我们如何实现界面间的沟通呢?我之前的做法就是在每个我要用到的面板上都写一个脚本实现对应的功能,相信不少新手开发者都是这样过来的吧。(纳尼菜逼竟是我自己?)
虽然这样做功能是实现了,但是维护性降低了、重复的代码也变多了,为了更加方便的管理和调用场景中的面板 我们需要写一套完整的框架来对它们进行约束不然很容易出现面板共存,或者切换失败等问题。
恰好SIKI老师有一套面向新手的小型UI框架课程讲得蛮好的,最重要的是简单啊!!! 对我这种新手来说真的非常友好!我们一起来学习下吧!
框架的思路
先来简单介绍下这个框架的思路吧 ,框架主要实现了面板的读取存储和管理,帮助我们更好的进行面板之间的切换管理
首先
1、我们通过Json文件存储我们所有要用到的面板信息 例如:面板的名字 面板的类型 面板的加载路径 等等,按实际项目需求为准
2、创建一个面板抽象类(就是所有面板的基类)来实现面板共有的一些方法,比如面板进入的时候要做的一些事情,面板退出时候要做的一些事情
3、通过代码读取Json文件得到所有面板的信息,然后我们就能通过这些信息来操作面板
4、框架最核心的地方就是如何高效的管理这些面板 先来走一遍我们面板的生成和关闭的流程

此时玩家只能操作背包面板,无法操作人物面板(想想是不是很多游戏都是这样)当我们想要操作人物面板的时候 我们需要先关闭背包面板然后才能操作人物面板
这里再画一幅图来解释
现在我们位于最上面的设置菜单中子菜单界面,此时我们只能操作子菜单界面,如果我们想操作设置菜单界面的话 我们需要先关闭子菜单,才能操作设置菜单

现在子菜单关闭了,我们就可以操作设置菜单了!

同理如果我们想操作主菜单 那我们就必须要关闭设置菜单才能去操作主菜单
所以Siki老师是如何管理面板之间切换关系的呢?了解过数据结构的同学看了上面的图可能马上就懂了!对的就是用堆栈(Stack)来实现 ,堆栈是一个后进先出的对象集合 后进先出 先进后出?这不恰好可以用来存储管理我们的面板吗
先进后出 最先进入的是主菜单,所以我们把他放到了容器底部,然后玩家又点击了设置菜单,于是我们把他放到主菜单的上面,然后玩家又点击了设置菜单里的子菜单,我们把子菜单放到了设置菜单的上面

我们让玩家只能操作栈中的顶部面板,通过Push把面板入栈(面板进入) 通过Pop把顶部面板出栈(面板退出) 这样就轻松实现了面板之间的切换功能了
好吧说实话我觉得我讲的是一坨屎(可能只有我自己能听懂 推荐直接去看Siki老师讲的)
实操
思路理了一下!现在我们一步一步来做吧!
1、创建Json文件,把面板信息写入到Json中
首先我们来实现第一步
我们通过Json文件存储我们所有要用到的面板信息 例如:面板的名字 面板的类型 面板的加载路径 等等,按实际项目需求为准
在创建Json文件之前,我们需要先把要用到的面板做成预制体存放到Resources路径下稍后通过Resources来加载这些我们要用到的面板,这里我自己新建了一张图作为面板来演示,命名为“SettingPanel”存放到Resources的UIPanel文件夹下

好现在我们就来创建一个Json文件(新建一个文本文件 后缀改成.Json就是一个Json文件了,注意我们Json文件也要放到Resources文件夹下因为我们稍后会通过Resources来获取它) 并往里面写上我们面板的信息,这里为了方便小伙伴们的理解我就写简单一点
json文件
{
"data":
[
{"uiPanelType":"settingPanel","uiPanelPath":"UIPanel/SettingPanel"}
]
}
这里简单讲下Json文件的格式吧
花括号{} 保存对象:对象可以包含各种数据,包括数组。
方括号[] 保存数组:数组可以包含对象。
数据由逗号分隔
不懂的我们可以直接找个在线解析Json的网站 这里推荐 https://www.bejson.com/convert/json2csharp/
我们直接选择Json转C#实体类看看 我们把写好的Json数据复制过去然后点击生成实体类看看

为了方便观察 我把生成的实体来复制过来给大伙看看
public class DataItem
{
public string uiPanelType { get; set; }
public string uiPanelPath { get; set; }
}
public class Root
{
public List <DataItem> data { get; set; }
}
不知道细心的小伙伴发现了没有 这两个类是不是刚好映射了Json文件里的数据
仔细对比一下 就能了解其中的意思
要注意的是 这些数据必须要和我们写的Json文件里的信息对应(反过来也是一样的 总之Json和映射类的信息名一定要保持一致) 只要写错一个单词 Json就会解析失败
//解析成功
public string uiPanelType { get; set; }
public string uiPanelPath { get; set; }
public List <DataItem> data { get; set; }
"uiPanelType":"settingPanel","uiPanelPath":"UIPanel/SettingPanel"}
//解析失败 映射类和Json的数据名称不一样 解析会报错
public string PanelType { get; set; }
public string PanelPath { get; set; }
public List <DataItem> paneldata { get; set; }
"uiPanelType":"settingPanel","uiPanelPath":"UIPanel/SettingPanel"}
我讲的可能跟一坨屎一样 甚至更差 (我帮你们讲了) 最好就是自己去度娘或者谷爹查下资料,现在我也就会最基本的操作回头我再深入学一下然后再写一篇关于Json的文章出来(祸害社会~)
刚刚是我们一键生成的映射类 现在我们自己动手来写下吧
PanelData类
public class PanelData
{
/// <summary>
/// UI面板类型
/// </summary>
public string uiPanelType;
/// <summary>
/// UI面板路径
/// </summary>
public string uiPanelPath;
}
UIPanelDataList类
public class UIPanelDataList
{
public PanelData[] data;
}
两个映射类写完了 我们还要写一个UIPanelType枚举类 用来定义我们的面板类型,到时候我们拿到Json文件里的uiPanelType数据,然后将String转换成枚举类型就能得到面板的类型了
UIPanelType类:
public enum UIPanelType
{
settingPanel //设置面板
}
2、创建面板的基类
创建一个面板抽象类(就是所有面板的基类)来实现面板共有的一些方法,比如面板进入的时候要做的一些事情,面板退出时候要做的一些事情
UIPanelBase类比较简单 没什么好讲了 根据自己的需求定义面板共有的一些方法
UIPanelBase类:
using UnityEngine;
public class UIPanelBase : MonoBehaviour
{
/// <summary>
/// 入栈状态
/// </summary>
public virtual void PushState()
{
Debug.Log("入栈状态");
}
/// <summary>
/// 出栈
/// </summary>
public virtual void PopState()
{
Debug.Log("出栈状态");
}
/// <summary>
/// 面板恢复
/// </summary>
public virtual void RemotState()
{
Debug.Log("恢复状态");
}
}
3、读取Json文件
通过代码读取Json文件得到所有面板的信息,然后我们就能通过这些信息来操作面板
这里我推荐使用LitJson,当然大家也可以用Unity自带的JsonUtility,哪个顺手就用那个。
要使用LitJson就需要去引用它 下载litJson.dll(这里我就不放下载链接了 Github上或者百度一下就有了) 放到Plugins文件夹下 如果没有就自己创建一个

现在我们可以创建核心的UIManager类
MainUIManager类
首先第一步引用litJson 非常简单就一句话 “using LitJson;”
然后把MainUIManager类做成一个单例(不懂单例的可以百度看看)
using System.Collections.Generic;
using UnityEngine;
using LitJson;
public class MainUIManager : MonoBehaviour
{
/// <summary>
/// MainUIManager单例
/// </summary>
public static MainUIManager Instance;
public void Awake()
{
Instance = this;
}
}
单例做完后 我们需要两个字典 一个用来存储从Json解析出来的数据 另一个用来存储面板的UIPanelBase(我们需要在每个要操作的面板上添加一个脚本并继承UIPanelBase类 我们拿到面板身上的UIPanelBase类就相当于拿到了面板的实例)
using System.Collections.Generic;
using UnityEngine;
using LitJson;
public class MainUIManager : MonoBehaviour
{
/// <summary>
/// MainUIManager单例
/// </summary>
public static MainUIManager Instance;
/// <summary>
/// 保存从Json文件解析出来的面板数据
/// </summary>
private Dictionary<UIPanelType, string> oriPanelDataDic = new Dictionary<UIPanelType, string>();
/// <summary>
/// 保存面板实例
/// </summary>
private Dictionary<UIPanelType, UIPanelBase> newPanelDataDic = new Dictionary<UIPanelType, UIPanelBase>();
public void Awake()
{
Instance = this;
}
}
我英文不是太好 取名会有点差
现在我们就可以来读取Json文件里的数据了
using System.Collections.Generic;
using UnityEngine;
using LitJson;
public class MainUIManager : MonoBehaviour
{
/// <summary>
/// MainUIManager单例
/// </summary>
public static MainUIManager Instance;
/// <summary>
/// 保存从Json文件解析出来的面板数据
/// </summary>
private Dictionary<UIPanelType, string> oriPanelDataDic = new Dictionary<UIPanelType, string>();
/// <summary>
/// 保存面板实例
/// </summary>
private Dictionary<UIPanelType, UIPanelBase> newPanelDataDic = new Dictionary<UIPanelType, UIPanelBase>();
/// <summary>
/// 文本资源 用于读取Json文件里的数据
/// </summary>
private TextAsset uiPanelJson;
public void Awake()
{
Instance = this;
}
/// <summary>
/// 初始化Json数据
/// </summary>
private void InitJson()
{
//获取Json文件数据
uiPanelJson = Resources.Load<TextAsset>("UIPanelData");
//解析Json
UIPanelDataList panelData = JsonMapper.ToObject<UIPanelDataList>(uiPanelJson.text);
//存储数据到字典中
foreach (PanelData item in panelData.data)
{
//将String类型转换成枚举类型 并存储到字典中
oriPanelDataDic.Add((UIPanelType)System.Enum.Parse(typeof(UIPanelType), item.uiPanelType.ToString()), item.uiPanelPath);
}
}
}
接下来我们拿到Canvas的Transform组件 等等我们写生成面板的逻辑需要把面板的父物体设置到Canvas下 Canvas我设置了个Tag 叫“UICanvas”所以我直接通过Tag去查找它
using System.Collections.Generic;
using UnityEngine;
using LitJson;
public class MainUIManager : MonoBehaviour
{
/// <summary>
/// MainUIManager单例
/// </summary>
public static MainUIManager Instance;
/// <summary>
/// 保存从Json文件解析出来的面板数据
/// </summary>
private Dictionary<UIPanelType, string> oriPanelDataDic = new Dictionary<UIPanelType, string>();
/// <summary>
/// 保存面板实例
/// </summary>
private Dictionary<UIPanelType, UIPanelBase> newPanelDataDic = new Dictionary<UIPanelType, UIPanelBase>();
/// <summary>
/// 文本资源 用于读取Json文件里的数据
/// </summary>
private TextAsset uiPanelJson;
private Transform uiCanvasTran;
public void Awake()
{
Instance = this;
InitJson();
Init();
}
/// <summary>
/// 初始化Json数据
/// </summary>
private void InitJson()
{
//获取Json文件数据
uiPanelJson = Resources.Load<TextAsset>("UIPanelData");
//解析Json
UIPanelDataList panelData = JsonMapper.ToObject<UIPanelDataList>(uiPanelJson.text);
//存储数据到字典中
foreach (PanelData item in panelData.data)
{
//将String类型转换成枚举类型 并存储到字典中
oriPanelDataDic.Add((UIPanelType)System.Enum.Parse(typeof(UIPanelType), item.uiPanelType.ToString()), item.uiPanelPath);
}
}
/// <summary>
/// 初始化操作
/// </summary>
private void Init()
{
if (uiCanvasTran == null)
{
uiCanvasTran = GameObject.FindGameObjectWithTag("UICanvas").transform;
}
}
}
接下来写生成面板的逻辑 也是很简单的
using System.Collections.Generic;
using UnityEngine;
using LitJson;
public class MainUIManager : MonoBehaviour
{
/// <summary>
/// MainUIManager单例
/// </summary>
public static MainUIManager Instance;
/// <summary>
/// 保存从Json文件解析出来的面板数据
/// </summary>
private Dictionary<UIPanelType, string> oriPanelDataDic = new Dictionary<UIPanelType, string>();
/// <summary>
/// 保存面板实例
/// </summary>
private Dictionary<UIPanelType, UIPanelBase> newPanelDataDic = new Dictionary<UIPanelType, UIPanelBase>();
/// <summary>
/// 文本资源 用于读取Json文件里的数据
/// </summary>
private TextAsset uiPanelJson;
private Transform uiCanvasTran;
public void Awake()
{
Instance = this;
InitJson();
Init();
}
/// <summary>
/// 初始化Json数据
/// </summary>
private void InitJson()
{
//获取Json文件数据
uiPanelJson = Resources.Load<TextAsset>("UIPanelData");
//解析Json
UIPanelDataList panelData = JsonMapper.ToObject<UIPanelDataList>(uiPanelJson.text);
//存储数据到字典中
foreach (PanelData item in panelData.data)
{
//将String类型转换成枚举类型 并存储到字典中
oriPanelDataDic.Add((UIPanelType)System.Enum.Parse(typeof(UIPanelType), item.uiPanelType.ToString()), item.uiPanelPath);
}
}
/// <summary>
/// 初始化操作
/// </summary>
private void Init()
{
if (uiCanvasTran == null)
{
uiCanvasTran = GameObject.FindGameObjectWithTag("UICanvas").transform;
}
}
}
/// <summary>
/// 生成面板
/// </summary>
/// <param name="panelType">面板类型</param>
/// <returns></returns>
public UIPanelBase GeneratePanel(UIPanelType panelType)
{
UIPanelBase uiPanelBase;
newPanelDataDic.TryGetValue(panelType, out uiPanelBase);
//如果uiPanelBase为空就表示没在字典中
if (uiPanelBase == null)
{
string panelPath;
oriPanelDataDic.TryGetValue(panelType, out panelPath);
GameObject newPanel = Instantiate(Resources.Load<GameObject>(panelPath));
newPanel.transform.SetParent(uiCanvasTran, false);
uiPanelBase = newPanel.GetComponent<UIPanelBase>();
newPanelDataDic.Add(panelType, uiPanelBase);
return uiPanelBase;
}
return uiPanelBase;
}
生成面板并返回得到uiPanelBase(面板基类)后我们就可以做面板入栈出栈的操作了
using System.Collections.Generic;
using UnityEngine;
using LitJson;
public class MainUIManager : MonoBehaviour
{
/// <summary>
/// MainUIManager单例
/// </summary>
public static MainUIManager Instance;
/// <summary>
/// 保存从Json文件解析出来的面板数据
/// </summary>
private Dictionary<UIPanelType, string> oriPanelDataDic = new Dictionary<UIPanelType, string>();
/// <summary>
/// 保存面板实例
/// </summary>
private Dictionary<UIPanelType, UIPanelBase> newPanelDataDic = new Dictionary<UIPanelType, UIPanelBase>();
/// <summary>
/// 文本资源 用于读取Json文件里的数据
/// </summary>
private TextAsset uiPanelJson;
private Transform uiCanvasTran;
private Stack<UIPanelBase> panelStack;
public void Awake()
{
Instance = this;
InitJson();
Init();
}
/// <summary>
/// 初始化Json数据
/// </summary>
private void InitJson()
{
//获取Json文件数据
uiPanelJson = Resources.Load<TextAsset>("UIPanelData");
//解析Json
UIPanelDataList panelData = JsonMapper.ToObject<UIPanelDataList>(uiPanelJson.text);
//存储数据到字典中
foreach (PanelData item in panelData.data)
{
//将String类型转换成枚举类型 并存储到字典中
oriPanelDataDic.Add((UIPanelType)System.Enum.Parse(typeof(UIPanelType), item.uiPanelType.ToString()), item.uiPanelPath);
}
}
/// <summary>
/// 初始化操作
/// </summary>
private void Init()
{
if (uiCanvasTran == null)
{
uiCanvasTran = GameObject.FindGameObjectWithTag("UICanvas").transform;
}
}
/// <summary>
/// 生成面板
/// </summary>
/// <param name="panelType">面板类型</param>
/// <returns></returns>
public UIPanelBase GeneratePanel(UIPanelType panelType)
{
UIPanelBase uiPanelBase;
newPanelDataDic.TryGetValue(panelType, out uiPanelBase);
//如果uiPanelBase为空就表示没在字典中
if (uiPanelBase == null)
{
string panelPath;
oriPanelDataDic.TryGetValue(panelType, out panelPath);
GameObject newPanel = Instantiate(Resources.Load<GameObject>(panelPath));
newPanel.transform.SetParent(uiCanvasTran, false);
uiPanelBase = newPanel.GetComponent<UIPanelBase>();
newPanelDataDic.Add(panelType, uiPanelBase);
return uiPanelBase;
}
return uiPanelBase;
}
/// <summary>
/// 面板入栈
/// </summary>
/// <param name="panelType"></param>
public void PushPanel(UIPanelType panelType)
{
if(panelStack==null)
{
panelStack = new Stack<UIPanelBase>();
}
UIPanelBase uiPanelBase = GeneratePanel(panelType);
uiPanelBase.PushState();
panelStack.Push(uiPanelBase);
}
/// <summary>
/// 面板出栈
/// </summary>
public void PopPanel()
{
if (panelStack == null) return;
//拿到顶部数据
UIPanelBase currentUIpanelBase = panelStack.Pop();
currentUIpanelBase.PopState();
if (panelStack.Count > 0)
{
UIPanelBase outUIpanelBase = panelStack.Peek();
outUIpanelBase.RemotState();
}
}
}
这样我们的MainUIManager类就写完了 接下来调用就很简单了 我们为SettingPanel添加一个脚本“SettingPanel”(记得继承UIPanelBase)
public class SettingPanel : UIPanelBase
{
public void AddButtonClick()
{
MainUIManager.Instance.PushPanel(UIPanelType.settingPanel);
}
}
方法写好后 把他绑定到要点击的Button中(例如 点击设置按钮 显示设置面板 就把设置面板的脚本绑定到设置按钮的Button组件中)

啊不会绑定 直接把面板拖到OnClick上然后选择我们刚刚写的方法就好了 或者百度一下马上就懂了
现在我们所有的步骤都完成了 运行游戏看看效果吧!正常来说我们现在点击设置按钮就会弹出面板的了
4、最后
如果你弹出了面板 那就证明成功了,如果没弹出也不要灰心好好研究下代码看看报错,在上面的案例中我们只做了面板入栈的操作,你可以试着完成下面板出栈的操作,非常简单的只需要写一行代码就好了,写代码就是要思考为什么,而不是什么都跟着敲,试着自己在这套框架上添加一些新的功能吧!
最后还是要说明下 文章主要是用来巩固我学到的知识用的,所以没那些大佬讲的这么详细,要是把你看懵了!那我就非常抱歉了,建议关掉去看一遍完整的教程哈哈哈哈~