前几天练习写了这一套Unity的UI框架,对于游戏开发很实用,写篇笔记梳理一下大致思路
工程文件见 github
点击对应按钮可以打开具体页面,当有具体页面的时候主菜单按钮无法点击,点击右上角的关闭按钮可以关闭该页面,关闭页面后主菜单按钮继续生效
这一部分是用于描述UI面板属性的一些类,分别是UIPanelType、UIPanel、BasePanel
这是一个枚举类,用于描述UIPanel的种类。在这里我写了四种,分别是主菜单面板MainMenuPanel、商店面板StorePanel、暂停面板PausePanel和系统设置面板SystemSettingPanel(注意:面板的prefab名称需要与枚举类的类型名称相同)
public enum UIPanelType
{
MainMenuPanel,
SystemSettingPanel,
PausePanel,
StorePanel
}
这个类用于储存面板本身文件的信息,有两个字段,一个字段用来描述该面板UIPanelType的类型,一个字段储存该面板prefab文件的路径,以便在游戏运行时动态实例化面板
public class UIPanel
{
public UIPanelType UIPanelType;
public string UIPanelPath;
}
这是所有面板上挂载的Script的基类,所以需要继承自MonoBehaviour。BasePanel里的方法用来描述所有面板共同的一些基本行为,例如打开与关闭,而面板的四个状态:进入场景OnEnter、暂停OnPause、继续OnResume(解除暂停)、推出场景OnExit也是属于这个类的四个方法。
方法的具体实现以及其他字段方法待后期需要再添加。
using UnityEngine;
using UnityEngine.UI;
public class BasePanel : MonoBehaviour
{
public void OnEnter()
{
}
public void OnPause()
{
}
public void OnResume()
{
}
public void OnExit()
{
}
}
UIManager类的该UI框架的核心,它负责的工作如下:
1.自动更新Json文件记载的指定的面板prefab文件夹下的prefab的UIPanelType与路径
2.使用列表储存所有UIPanel
3.使用字典储存所有UIPanel上挂载的继承自BasePanel的组件/script
4.使用栈储存场景中正在显示的UIPanel
把UIManager做成单例模式
private static UIManager _instance;
public static UIManager Instance
{
get
{
if (_instance == null)
_instance = new UIManager();
return _instance;
}
}
首先引入LitJson库,然后引入命名空间using LitJson
指定面板prefab所存放的文件夹,这里我放在Resources下的UIPanelPrefab文件夹下,方便Unity打包与加载
readonly string panelPrefabPath = Application.dataPath + @"/Resources/UIPanelPrefab";
指定Json文件所存放的文件夹,因为Resources文件夹在游戏打包后是只读的,需要写入操作的Json文件就需要单独另开一个文件夹,这里我放在Json文件夹下,Json文件命名为UIJson
readonly string jsonPath = Application.dataPath + @"/Json/UIJson.json";
使用List列表储存所有的UIPanel
private List<UIPanel> panelList;
写一个传入Json路径,就可以读取Json文件并转换为List类型的列表返回的方法
public List<UIPanel> ReadJsonFile(string jsonPath)
{
//如果找不到UIJson文件,则新建一个Json文件并写入“[]”
//如果仅新建一个空Json文件,Json转换会返回一个null,也就是后面的list等于null,之后使用list的操作就会报一个空指针错误。
if (!File.Exists(jsonPath))
File.WriteAllText(jsonPath, "[]");
List<UIPanel> list = JsonMapper.ToObject<List<UIPanel>>(File.ReadAllText(jsonPath));
return list;
}
再写一个传入Json路径和List类型的UIPanel的列表,可以把该列表内容写入该路径的Json文件的方法
public void WriteJsonFile(string jsonPath, List<UIPanel> list)
{
string json = JsonMapper.ToJson(list);
File.WriteAllText(jsonPath, json);
}
最后写自动更新Json文件的方法UIPanelInfoSaveInJson()
public void UIPanelInfoSaveInJson()
{
panelList = ReadJsonFile(jsonPath);//读取现有json里的UIPanelList
//读取储存面板prefab的文件夹的信息
DirectoryInfo folder = new DirectoryInfo(panelPrefabPath);
//遍历储存面板prefab的文件夹里每一个prefab的名字,并把名字转换为对应的UIPanelType
//再检查UIPanelType是否存在于List里,若存在List里则更新path,若不存在List里则加上
foreach (FileInfo file in folder.GetFiles("*.prefab"))
{
UIPanelType type = (UIPanelType)Enum.Parse(typeof(UIPanelType), file.Name.Replace(".prefab", ""));
//这里的path是相对路径,因为加载时使用的是Resources.load
string path = @"UIPanelPrefab/" + file.Name.Replace(".prefab", "");
bool UIPanelExistInList = false;//默认该UIPanel不在现有UIPanelList中
//在List里查找type相同的UIPanel
//SearchPanelForType是一个List的扩展方法,具体见Extension部分
UIPanel uIPanel = panelList.SearchPanelForType(type);
if (uIPanel != null)//UIPanel在该List中,更新path值
{
uIPanel.UIPanelPath = path;
UIPanelExistInList = true;
}
if (UIPanelExistInList == false)//UIPanel不在List中,加上该UIPanel
{
UIPanel panel = new UIPanel
{
UIPanelType = type,
UIPanelPath = path
};
panelList.Add(panel);
}
}
WriteJsonFile(jsonPath, panelList);//把更新后的UIPanelList写入Json
AssetDatabase.Refresh();//刷新资源
}
UIPanelInfoSaveInJson()需要在UIManager的构造方法里调用,因为需要在游戏运行开始就更新Json文件,以免后面加载的仍然是旧Json文件里记录的prefab路径
private UIManager()//【单例模式】构造方法为私有,UIManager的实例仅能在类内构造
{
//自动解析Json文件并赋值给panelList
//并使用prefab文件夹下的信息对panelList进行更新,再写入json文件
UIPanelInfoSaveInJson();
}
使用字典储存 面板类型UIPanelType :实例面板BasePanel 键值对
private Dictionary<UIPanelType, BasePanel> panelDict;
开发一个给出面板类型,就实例化该面板,并返回面板上挂载的BasePanel组件的方法
先获得场景中的Canvas,以便实例化的面板设为Canvas的子物体
private Transform canvasTransform;
public Transform CanvasTransform
{
get
{
if (canvasTransform == null)
//通过名称获取Canvas上的Transform,所以不要有同名Canvas
canvasTransform = GameObject.Find("Canvas").transform;
return canvasTransform;
}
}
GetPanel方法
public BasePanel GetPanel(UIPanelType type)
{
if (panelDict == null)
panelDict = new Dictionary<UIPanelType, BasePanel>();
//【扩展发放】通过type查找字典里对应的BasePanel,若找不到则返回null,具体见Extension部分
BasePanel panel = panelDict.TryGetValue(type);
//在现有字典里没有找到
//只能去json里找type对应的prefab的路径并加载
//再加进字典里以便下次在字典中查找
if (panel == null)
{
//【扩展方法】通过Type查找列表里对应的UIPanel,若找不到则返回null,具体见Extension部分
string path = panelList.SearchPanelForType(type).UIPanelPath;
if (path == null)
throw new Exception("找不到该UIPanelType的Prefab");
if (Resources.Load(path) == null)
throw new Exception("找不到该Path的Prefab");
//实例化prefab
GameObject instPanel = GameObject.Instantiate(Resources.Load(path)) as GameObject;
//把面板设为Canvas的子物体,false表示不保持worldPosition,而是根据Canvas的坐标设定localPosition
instPanel.transform.SetParent(CanvasTransform, false);
panelDict.Add(type, instPanel.GetComponent<BasePanel>());
return instPanel.GetComponent<BasePanel>();
}
return panel;
}
接下来就可以进行UIManager的最后一项工作——开发一个栈来储存当前场景中正在显示的Panel
因为面板是后打开的面板先关闭,栈是最适合储存的数据结构
private Stack<BasePanel> panelStack;
开发把指定类型的Panel入栈的方法PushPanel()
//把指定类型的panel入栈,并显示在场景中
public void PushPanel(UIPanelType type)
{
if (panelStack == null)
panelStack = new Stack<BasePanel>();
//判断栈里是否有其他panel,若有,则把原栈顶panel设定其状态为暂停(OnPause)
if (panelStack.Count > 0)
{
BasePanel topPanel = panelStack.Peek();
topPanel.OnPause();
}
BasePanel panel = GetPanel(type);
//把指定类型的panel入栈并设定其状态为进入场景(OnEnter)
panelStack.Push(panel);
panel.OnEnter();
}
开发把栈顶的Panel出栈的方法PopPanel()
//把栈顶panel出栈,并从场景中消失
public void PopPanel()
{
if (panelStack == null)
panelStack = new Stack<BasePanel>();
//检查栈是否为空,若为空则直接退出方法
if (panelStack.Count <= 0) return;
//把栈顶panel出栈,并把该panel状态设为退出场景(OnExit)
BasePanel topPanel = panelStack.Pop();
topPanel.OnExit();
//再次检查出栈栈顶Panel后栈是否为空
//若为空,直接退出方法
//若不为空,则把新的栈顶Panel状态设为继续(OnResume)
if (panelStack.Count <= 0) return;
BasePanel topPanel2 = panelStack.Peek();
topPanel2.OnResume();
}
至此,UIManager开发完毕。
这个类负责在游戏开始的时候加载所有需要的游戏配置,在该UI框架里,GameRoot负责在游戏刚开始运行时加载主菜单面板MainMenuPanel。
由于这个类需要挂在场景中的物体上,所以需要继承自MonoBehaviour
using UnityEngine;
public class GameRoot : MonoBehaviour
{
void Start ()
{
//在游戏开始时加载MainMenuPanel
UIManager.Instance.PushPanel(UIPanelType.MainMenuPanel);
}
}
该部分是对基类库的一些类的扩展,在这里扩展了List列表类和Dictionary字典类
using System.Collections.Generic;
public static class ListExtension
{
///
/// 扩展List类
/// 查找字段是指定UIPanelType的UIPanel,返回UIPanel的引用
///
/// UIPanel的List
///
///
///
public static UIPanel SearchPanelForType(this List<UIPanel> list,UIPanelType type)
{
foreach (var item in list)
{
if (item.UIPanelType == type)
return item;
}
return null;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class DictionaryExtension
{
///
/// 扩展字典类中的TryGetValue方法
/// 可以直接通过给出key返回value,而不是像原方法一样返回bool值
///
///
///
///
///
///
public static TValue TryGetValue<TKey,TValue>(this Dictionary<TKey,TValue> dict,TKey key)
{
TValue value;
dict.TryGetValue(key, out value);
return value;
}
}
首先实现BasePanel里的四个状态的方法
我通过CanvasGroup组件来控制Panel的可否点击与出现消失
canvasGroup.alpha属性等于1时面板出现(OnEnter),等于0时面板消失(OnExit)
canvasGroup.blocksRaycasts属性等于true时面板可以点击(OnResume),等于false时面板不可点击(OnPause)
首先获得该组件,如果Panel上没有CanvasGroup组件,则添加该组件
public void Awake()
{
canvasGroup = GetComponent<CanvasGroup>();
if(canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
在进入状态(OnEnter)中,Panel是出现且可点击的
public void OnEnter()
{
canvasGroup.alpha = 1;
canvasGroup.blocksRaycasts = true;
}
在暂停状态(OnPause)中,Panel是出现且不可点击的
public void OnPause()
{
canvasGroup.alpha = 1;
canvasGroup.blocksRaycasts = false;
}
在继续状态(OnResume),Panel是出现且可点击的
public void OnResume()
{
canvasGroup.alpha = 1;
canvasGroup.blocksRaycasts = true;
}
在退出状态(OnExit),Panel是消失且不可点击的
public void OnExit()
{
canvasGroup.alpha = 0;
canvasGroup.blocksRaycasts = false;
}
如果有Panel的四个状态的逻辑与BasePanel中不同,或者希望加一些进入消失动画,可以在对应的派生类中对这四个方法进行override
大多数面板都有关闭按钮,所以在BasePanel里也可以写一个检测子物体中是否有一个叫做CloseButton的按钮,并绑定监听事件
//给出子物体名字,并尝试在子物体中搜索到以该名字命名的子物体的button组件
//搜索到了返回button组件,未搜索到则返回null
private Button FindCloseButton(string childName)
{
Button closeButton = null;
foreach (var item in GetComponentsInChildren<Button>())
{
if (item.name == childName)
closeButton = item.GetComponent<Button>();
}
return closeButton;
}
然后在Awake里绑定按钮点击监听事件,如果点击了名为CloseButton的物体上的Button,就让panelStack栈顶元素出栈(关闭当前页面最上方的页面)
public void Awake()
{
btn = FindCloseButton("CloseButton");
canvasGroup = GetComponent<CanvasGroup>();
if (btn != null)
{
btn.onClick.AddListener(UIManager.Instance.PopPanel);
}
if(canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
所以如果要为Panel设置关闭按钮,就要把关闭按钮命名为CloseButton,并作为Panel的子物体。
如果找不到CloseButton,这个页面就是无法关闭的,例如主菜单页面MainMenuPanel
public class MainMenuPanel : BasePanel
{
public void OnClickSystemSettingButton()
{
UIManager.Instance.PushPanel(UIPanelType.SystemSettingPanel);
}
public void OnClickStoreButton()
{
UIManager.Instance.PushPanel(UIPanelType.StorePanel);
}
public void OnClickPauseButton()
{
UIManager.Instance.PushPanel(UIPanelType.PausePanel);
}
}
UI框架搭建完毕,之后需要添加Panel的时候,只需要做好Prefab,然后在UIPanelType里添加其对应的枚举类型,并以此枚举类型为Prefab命名,然后把Prefab放到指定的文件夹里即可。