先说明该UI框架的作用是用来控制UI面板之间的相互跳转的,使用了UI框架后,最大的用处就可以避免页面切换时复杂的操作,使用UI框架可以更好的管理UI页面,控制页面的显示和关闭也分别只由一个函数控制,极大的优化了代码
先看一张UI框架图
结合上方的图,开始逐步制作UI框架
1.首先将UI的每个面板单独制作好,然后放在Resources文件中当做预制件(这样做的目的是为了在加载的时候就可以直接获取所有的面板(具体的实现是通过json解析在字典中),具体实现在下面)
2.创建一个枚举类型UIPanelType来保存对应面板
创建一个UIPanelType的json文件来保存面板和其对应的路径,注意这里的panelType的值一定要和枚举类型中的值保持一致,后面才能解析成功(需要通过Resources文件进行加载,因此要把它放在Resources文件中(可以再创建一个Resources文件))
3.创建UIManager类,在该类中有三个功能,
第一个功能:解析保存面板信息(通过解析json文件,然后存放在字典中,键为枚举的UIType,值为路径)(注意再解析json时,还需要一个数据类来接收,因此创建一个UIPanelInfo类来接收json的解析数据)注意这里的UIPanelInfo类就收是对应的[{},{}],但是可以看出来这个结构解析出来是一个数组或者list,但是unity解析只能解析出类(在从版本0.9.0开始,LitJSON支持将List对象序列化为JSON字符串,无需后面多创建的类)(litjson可以解析成数组,jsonutility不能解析成数组,具体参照文章一,文章二,文章三),因此需要将这个结构变为{ [ {},{},{} ] },将数组放在{}中,也就是还需要另一个类UIPanelTypeJson来表示最外层的{},注意在解析json时要使用字符串转枚举类型(因为不能直接解析成枚举)详情见字符串和枚举的转换(目前功能迭代只用uimanager),看完第一个功能看步骤4
Json文件的内容也随之改变成这样
第二个功能:创建并保存面板的实例(通过字典,键为面板的枚举,值为面板的BasePanel组件(可以通过子类对象来获取父类,用BasePanel来表示页面)),并创建方法(GetPanel())通过面板枚举获取该面板的父类(获取时有扩展字典的方法,请参照对Dictory的扩展)
第三个功能:管理保存所有显示的面板(通过栈的方式来保存显示的页面,在栈中都是显示的页面,栈顶的是可以操作的页面,也就是可以点击的页面,栈顶下面的页面是只显示不能操作),因此有创建栈,出栈和入栈(这两个最重要,因为所有页面的显示和关闭都是依靠这个出栈和入栈)的操作(还有下方步骤5的状态添加)
4.创建一个面板的公共基类BasePanel,让所有的面板都继承自BasePanel,因为每个面板的脚本都继承自BasePanel(这里的面板类是自己创建的),这样在实例化完每个面板后,可以直接通过面板的实例化对象.GetComponent
面板就是这些,根据自己项目创建,在最下方附上面板类代码:
5.每个页面存在四种状态分别是OnEnter(页面显示出来),OnPause(页面暂停,因为弹出了其它页面),OnResume(页面继续,覆盖在该页面上的其它页面移除,恢复本页面的交互),OnExit(页面移除,移除该页面的显示)详情见流程图,注意因为每个页面都有这四种状态,因此把这四种状态定义在BasePanel中,通过虚方法,然后让每个子类重写,其中可以参考背包面板,都涉及到了这四个状态(如果像添加显示和退出动画,那就在OnEnter和OnExit中添加)
6.为各个面板上的各个按钮添加点击事件(在继承自BasePanel的脚本中进行),比如我现在有个主菜单,脚本为MainPanel,主菜单上有任务,背包,等各种按钮,这个时候UI框架就发挥作用了,可以直接在按钮点击事件中通过UIManager中的入栈方法来显示页面,用出栈方法来关闭页面等,记得也需要通面板的枚举类型(这里点击事件不能传枚举类型的参数,因此还是需要字符串转枚举类型这一步),就像这样
7.就是GameRoot部分,就用来启动UI框架
下面附上代码部分,按照上方介绍的先后顺序
UIPanelType类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum UIPanelType
{
ItemMessage,
Knapsack,
MainMenu,
Shop,
Skill,
System,
Task
}
UIPanelType的json文件:
{
"infoList": [
{
"panelTypeString": "ItemMessage",
"path": "UIPanel/ItemMessagePanel"
},
{
"panelTypeString": "Knapsack",
"path": "UIPanel/KnapsackPanel"
},
{
"panelTypeString": "MainMenu",
"path": "UIPanel/MainMenuPanel"
},
{
"panelTypeString": "Shop",
"path": "UIPanel/ShopPanel"
},
{
"panelTypeString": "Skill",
"path": "UIPanel/SkillPanel"
},
{
"panelTypeString": "System",
"path": "UIPanel/SystemPanel"
},
{
"panelTypeString": "Task",
"path": "UIPanel/TaskPanel"
}
]
}
UIManager类(框架的核心内容)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIManager
{
#region 第一部分功能
private Dictionary panelPathDict;//面板信息字典,用来存放面板所对应的Prefab路径
private static UIManager _instance;
public static UIManager Instance//单例模式不是这章重点,不做讲解
{
get
{
if(_instance == null)
{
_instance = new UIManager();
}
return _instance;
}
}
private UIManager()
{
ParseUIPanelTypeJson();
}
[Serializable]
class UIPanelTypeJson
{
public List infoList;
}
private void ParseUIPanelTypeJson()//解析json文件
{
panelPathDict=new Dictionary();
TextAsset ta = Resources.Load("UIPanelType");//json文件在unity中是属于TextAsset类,因此用这个类来接收
//这里应该也可以用txt文件保存json数据,然后用读取流的形式读取
UIPanelTypeJson jsonObject = JsonUtility.FromJson(ta.text);//注意,如果这样的话就会报错,原因在解析json的时候,并不能解析成枚举类型,因为在UIPanelInfo类的panelType成员为枚举类型,因此就会报解析错误
//将解析的json信息,填入panelPathDict字典中,键对应面板枚举值,值对应面板路径
foreach (UIPanelInfo info in jsonObject.infoList)
{
panelPathDict.Add(info.panelType, info.path);
}
}
#endregion
#region 第二部分功能
private Dictionary panelDict;//保存所有实例化面板的游戏物体身上的脚本组件(继承自BasePanel的)
private Transform canvasTransform;//获取面板
public Transform CanvasTransform
{
get
{
if(canvasTransform == null)
{
canvasTransform = GameObject.Find("Canvas").transform;
}
return canvasTransform;
}
}
private BasePanel GetPanel(UIPanelType panelType)//根据面板的枚举类型,得到实例化的面板
{
if (panelDict == null)//判断字典是否为空
{
panelDict = new Dictionary();//如果为空就创建字典
}
// BasePanel panel;
//panelDict.TryGetValue(panelType, out panel);
BasePanel panel = panelDict.TryGet(panelType);//字典扩展方法
if (panel == null)//判断是否得到该面板
{
//如果为空就根据panelPathDict字典中路径实例化面板,并保存在panelDict中和放在Canvas画布下
//string path;
// panelPathDict.TryGetValue(panelType, out path);
string path = panelPathDict.TryGet(panelType);
//GameObject instPanelPrefab = Resources.Load(path);//这是通过动态加载的形式获取到了面板预制件的引用
//GameObject instPanel=GameObject.Instantiate(instPanelPrefab);//因为没有继承自MonoBehaviour,所有需要加GameObject
GameObject instPanel = GameObject.Instantiate(Resources.Load(path));//这是简洁写法
panelDict.Add(panelType, instPanel.GetComponent());
instPanel.transform.SetParent(CanvasTransform, false);//放在画布下
return instPanel.GetComponent();
}
else
{
return panel;
}
}
#endregion
#region 第三部分功能
private Stack panelStack;
//都是根据页面的枚举类型来获取到页面,才能方便出栈入栈操作
public void PushPanel(UIPanelType panelType )//把页面入栈,把某个页面显示在界面上
{
if(panelStack == null)
{
panelStack = new Stack();
}
//判断栈里面是否有页面
if(panelStack.Count > 0 )
{
BasePanel topPanel=panelStack.Peek();//这是获取到栈顶元素Pop是出栈
topPanel.OnPause();//将栈顶元素暂停
}
BasePanel panel=GetPanel(panelType);//这里就用到了第二部分功能中的获取面板的方法
panelStack.Push(panel);//入栈
panel.OnEnter();//这是新的栈顶元素显示
//注意这里都只是统一调用BasePanel中的方法,因为BasePanel中的方法是虚方法没有具体实现,需要在子类中具体实现后,就能有具体的功能
}
public void PopPanel()//把页面出栈, 把页面从界面上移除
{
if (panelStack == null)
{
panelStack = new Stack();
}
if (panelStack.Count <= 0)
{
return;
}
BasePanel topPanel=panelStack.Pop();//栈顶出栈
topPanel.OnExit();
if (panelStack.Count <= 0)//这是如果关闭页面后下方还有页面就需要将栈顶页面恢复
{
return;
}
BasePanel topPanel2 = panelStack.Peek();
topPanel2.OnResume();//这里同理,具体实现都是在子类脚本中重新,这里只负责调用
}
#endregion
}
UIPanelInfo类:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class UIPanelInfo:ISerializationCallbackReceiver
{
[NonSerialized]//因为json解析成不了枚举类型,因此就不解析成它
public UIPanelType panelType;//注意,因为这个类时用来接收解析的json的,因此变量成员名一定要和json中的名字要相同
public string panelTypeString;//json解析成字符串
public string path;
public void OnAfterDeserialize()//反序列化成功之后调用
{
// throw new NotImplementedException();
//这里就是将字符串转化为枚举类型的过程,放在反序列化成功之后执行
UIPanelType Type = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), panelTypeString);
panelType = Type;
}
public void OnBeforeSerialize()//反序列化成功之前调用
{
// throw new NotImplementedException();
}
}
DictionaryExtension类:字典扩展类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class DictionaryExtension //字典扩展类,对字典使用方法进行扩展
{
public static Tvalue TryGet(this Dictionary dict,Tkey key)
{
Tvalue value;
dict.TryGetValue(key, out value);
return value;
}
}
BasePanel类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BasePanel : MonoBehaviour
{
///
/// 界面被显示出来
///
public virtual void OnEnter()
{
}
///
/// 界面暂停
///
public virtual void OnPause()
{
}
///
/// 界面继续
///
public virtual void OnResume()
{
}
///
/// 界面不显示,退出这个界面,界面被关系
///
public virtual void OnExit()
{
}
}
GameRoot类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameRoot : MonoBehaviour
{
private void Start()
{
UIManager.Instance.PushPanel(UIPanelType.MainMenu);
}
}
展示一部分项目面板的类(继承自BasePanel),不属于框架部分
主菜单类(挂载在主菜单页面上),其中的点击事件方法,如点击主菜单中的任务按钮显示任务页面就需要自己写函数再绑定到按钮上,这里就是OnClickPushPanel()函数
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainMenuPanel : BasePanel
{
private CanvasGroup canvasGroup;
private void Start()
{
canvasGroup = GetComponent();
}
//这里是重新的MainMenuPanel页面枚举类型对应的BasePanel中的OnPause方法,因为在UIManager中的panelDict保存了枚举类型对应的BasePanel
//因此这里的调用流程就是先调用下方的PushPanel,在PushPanel函数中根据panelType获取到BasePanel,然后执行 panel.OnPause();等方法,又因为
//获取到的BasePanel就是 MainMenuPanel的父类这个 BasePanel,因此调用的panel.OnPause();方法就是执行的下方的内容
public override void OnPause()//这里具体的暂停方法就是取消掉主页面中的所有可交互的东西,因此可以用CanvasGroup控制,详细使用方法见手册
{
canvasGroup.blocksRaycasts = false;//暂停该页面,让鼠标不再和该页面交互
}
public override void OnResume()
{
canvasGroup.blocksRaycasts = true;
}
public void OnClickPushPanel(string panelString )//定义点击事件,将要显示的页面添加进栈中,这里是因为要复用才通过字符串形式转换
{
UIPanelType panelType=(UIPanelType)System.Enum.Parse(typeof(UIPanelType), panelString);
UIManager.Instance.PushPanel( panelType );
}
}
再比如背包类:
using DG.Tweening;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class KnapsackPanel : BasePanel
{
private CanvasGroup canvasGroup;
private void Start()
{
if (canvasGroup == null) canvasGroup = GetComponent();
}
public override void OnEnter()
{
if (canvasGroup == null) canvasGroup = GetComponent();//这里是防止面板在实例化出来后,立马调用了OnEnter()方法,导致 canvasGroup还没赋值而报空
canvasGroup.alpha = 1;
canvasGroup.blocksRaycasts = true;
// gameObject.SetActive(true); //这里也可以直接可以设置页面的active来表示进入和退出
Vector3 temp = transform.localPosition;
temp.x = 1300;
transform.localPosition = temp;
transform.DOLocalMoveX(0, 0.5f);
}
public override void OnExit()//关闭该页面的具体实现内容
{
//canvasGroup.alpha = 0;
canvasGroup.blocksRaycasts = false;
transform.DOLocalMoveX(1800, 0.5f).OnComplete(() => { canvasGroup.alpha = 0; });
// gameObject.SetActive(false);
}
public override void OnPause()
{
canvasGroup.blocksRaycasts = false;
}
public override void OnResume()
{
canvasGroup.blocksRaycasts = true;
}
public void OnClosePanel()
{
UIManager.Instance.PopPanel();
}
public void OnItemButtonClick()//点击显示物品按钮
{
UIManager.Instance.PushPanel(UIPanelType.ItemMessage);
}
}
因此总结一下,属于UI框架的类就7个
其余的像KnapsackPanel 这些类都是依据项目变化而变化的