Unity3D通用UI框架设计,美术,策划,程序互不干扰,易于扩展,解决多个UI同时打开的层级问题,以及使用快捷键关闭最近打开的UI
本文介绍了一种通用的UI框架的设计方式,让策划,美术,程序的工作互不干扰,且资源发生改变时无需修改代码,只需替换对应的资源,易于扩展,并且可以使用快捷键快速关闭最近打开的UI
大家好我是开罗小8,这次我来介绍一下我设计的UI框架,先看一下实现的效果吧
实现效果:
1.按下快捷键可以关闭最近打开的面板
2.同时只能打开一个UI,打开新的时会临时隐藏上一个UI,但弹窗类可以叠加显示的UI不受限制
3.使用Excel表格配置UI的显示模式,并且UI资源动态加载
由于本人经验有限,可能存在设计不合理的地方,如果大家有更好的方法欢迎在评论区指出
设计分析
在团队合作中,往往是策划负责设计UI如何进行显示,美术负责绘画以及拼接UI,程序负责功能的实现,期间策划和美术可能会频繁的修改,为了避免每次修改视觉效果都需要修改程序,因此我设计的UI框架在策划,美术修改完毕后直接替换对应的数据和资源即可。
策划配置表
在配置表中
IsCache:标记当前UI是否需要缓存
IsDynamic:表示当前是否是动态UI,即UI的大小,位置等属性是否一直在变化,如果是动态UI会放到另一个Canvas中做到动静分离
OverlayMode:表示叠加模式,如果是Additive表示当前UI可以同时显示
ControlMode:表示当前UI玩家是否可以主动关闭,如果是Player表示当前UI玩家可以主动关闭
Layer:表示UI的层级,数值越大越优先显示
如何将Excel表格导出Json格式并解析参考我的上一篇文章:使用Excel制作游戏配置表并导出Json和C#代码到Unity中使用-CSDN博客
美术资源
美术资源只是已经拼好的UI界面的预制体,只包含UI基础组件(Image,Text,Button等),不包含任何脚本,预制体的名称需要和脚本的名称保持相同
代码设计
UI基类
UIBasePanel:每一个UI的基类,所有的UI都继承此脚本
public class UIBasePanel : MonoBehaviour
{
protected UIInfo UIInfo;//存储UI的信息
public bool IsShow { get; private set; }//当前UI是否正在展示
protected GameObject PanelRoot;//当前UI的根节点
public virtual void OnInit(UIInfo info)
{
UIInfo=info;
PanelRoot = transform.Find("Root").gameObject;
}
public virtual void OnOpen()//开启UI,由UIManager进行控制
{
if (!IsShow)
{
ShowUI();
}
}
public virtual void OnClose()//关闭UI,由UIManager进行控制
{
if (IsShow)
{
HideUI();
if (!UIInfo.IsCache)
{
Destroy(gameObject);
}
}
}
public virtual void ShowUI()//展示UI,播放UI入场动画
{
IsShow = true;
PanelRoot.SetActive(true);
}
public virtual void HideUI()//隐藏UI,播放UI关闭动画
{
IsShow = false;
PanelRoot.SetActive(false);
}
}
在OnInit函数中通过transform.Find动态获取对应的组件
UI管理器
UIManager:UI管理器,控制UI的开启与关闭等,使用单例模式
using System;
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
public class UIManager : Singleton<UIManager>
{
private Transform UIRoot;
private Transform StaticUIRoot;
private Transform DynamicUIRoot;
private Dictionary<string, UIInfo> UIInfos;
private List<UIInfo> OpenedUIList;
public UIManager()
{
Init();
}
private void Init()
{
UIInfos = new Dictionary<string, UIInfo>();
OpenedUIList = new List<UIInfo>();
CreateRoot();
LoadUIData();
Debug.Log("[UIManager Init]");
}
private void LoadUIData()
{
string jsonData=Resources.Load<TextAsset>(PathUtil.GetUIDataPath()).text;
UIInfos = JsonConvert.DeserializeObject<Dictionary<string, UIInfo>>(jsonData);
}
private void CreateRoot()
{
GameObject uiRootRes = Resources.Load<GameObject>("UIRoot");
if (uiRootRes == null)
{
Debug.LogError("未找到UIRoot资源");
}
UIRoot = GameObject.Instantiate(uiRootRes).transform;
StaticUIRoot = UIRoot.transform.Find("StaticUIRoot");
DynamicUIRoot = UIRoot.transform.Find("DynamicUIRoot");
GameObject.DontDestroyOnLoad(UIRoot);
}
/// <summary>
/// 打开指定UI面板
/// </summary>
/// <typeparam name="T">UI面板的类型</typeparam>
/// <returns>对应的UI面板实例</returns>
public UIBasePanel Open<T>()
{
string uiName = typeof(T).Name;
return Open(uiName);
}
public UIBasePanel Open(string uiName)
{
if (UIInfos.ContainsKey(uiName))
{
UIInfo uiInfo = UIInfos[uiName];
UIBasePanel panelInstance = uiInfo.PanelInstance;
if (uiInfo.IsOpen)
{
Debug.LogWarning($"{uiName}面板已经打开,将不会再次调用OnOpen方法");
}
else
{
if (panelInstance == null)
{
#region 实例化新的UI
GameObject uiRes = Resources.Load<GameObject>(PathUtil.GetUIPath(uiName));
if (uiRes == null)
{
Debug.LogError($"未找到对应路径下的UI资源:");
return null;
}
panelInstance = GameObject.Instantiate(uiRes).AddComponent(Type.GetType(uiName)) as UIBasePanel;
Transform targetTrans = null;
if (uiInfo.IsDynamic)
{
targetTrans = DynamicUIRoot.transform.Find(uiInfo.Layer.ToString());
}
else
{
targetTrans = StaticUIRoot.transform.Find(uiInfo.Layer.ToString());
}
if (targetTrans == null)
{
targetTrans = StaticUIRoot;
Debug.LogWarning("未找到UI对应的层级,请检查UIRoot每一个Canvas下是否包含所有UILayer的名称");
}
panelInstance.transform.SetParent(targetTrans, false);
panelInstance.OnInit(uiInfo);
UIInfos[uiName].PanelInstance = panelInstance;
#endregion
}
panelInstance.OnOpen();
panelInstance.transform.SetAsLastSibling();
uiInfo.IsOpen = true;
OnUIOpen(uiInfo);
}
return panelInstance;
}
else
{
Debug.LogError($"开启面板失败,{uiName}面板不存在");
return null;
}
}
/// <summary>
/// 关闭指定UI面板
/// </summary>
/// <typeparam name="T"></typeparam>
public void Close<T>() where T : UIBasePanel
{
string uiName = typeof(T).Name;
Close(uiName);
}
public void Close(string uiName)
{
if (UIInfos.ContainsKey(uiName))
{
UIInfo uiInfo = UIInfos[uiName];
if (uiInfo.IsOpen)
{
uiInfo.PanelInstance.OnClose();
uiInfo.IsOpen = false;
OnUIClose(uiInfo);
}
else
{
Debug.LogWarning($"关闭面板失败,{uiName}面板未开启");
}
}
else
{
Debug.LogError($"关闭面板失败,{uiName}面板不存在");
}
}
/// <summary>
/// 关闭最近打开的面板
/// </summary>
public bool CloseRecent()
{
int index = OpenedUIList.Count - 1;
while (index >= 0 && OpenedUIList[index].ControlMode == UIControlMode.System)
{
index--;
}
if (index >= 0)
{
Close(OpenedUIList[index].UIName);
return true;
}
return false;
}
public void CloseAll()
{
int count = OpenedUIList.Count;
for (int i = count - 1; i >=0; i--)
{
Close(OpenedUIList[i].UIName);
}
}
/// <summary>
/// 获取指定面板的实例
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public UIBasePanel GetPanel<T>() where T : UIBasePanel
{
string uiName = typeof(T).Name;
return GetPanel(uiName);
}
public UIBasePanel GetPanel(string uiName)
{
if (UIInfos.ContainsKey(uiName))
{
return UIInfos[uiName].PanelInstance;
}
else
{
Debug.LogError($"获取Panel失败:{uiName}不存在");
return null;
}
}
private void OnUIOpen(UIInfo uiInfo)
{
OpenedUIList.Add(uiInfo);
if (uiInfo.OverlayMode == UIOverlayMode.NewPanel)
{
//如果当前面板不是叠加的,先调用上一个非叠加面板的关闭
int lastPanelIndex = OpenedUIList.Count - 2;
while (lastPanelIndex >= 0 && OpenedUIList[lastPanelIndex].OverlayMode != UIOverlayMode.NewPanel)
{
--lastPanelIndex;
}
if (lastPanelIndex >= 0)
{
OpenedUIList[lastPanelIndex].PanelInstance.HideUI();
}
}
}
private void OnUIClose(UIInfo uiInfo)
{
if (uiInfo.OverlayMode == UIOverlayMode.NewPanel)
{
int index = OpenedUIList.IndexOf(uiInfo);
if (index == OpenedUIList.Count - 1)
{
int lastPanelIndex = OpenedUIList.Count - 2;
while (lastPanelIndex >= 0 && OpenedUIList[lastPanelIndex].OverlayMode != UIOverlayMode.NewPanel)
{
--lastPanelIndex;
}
if (lastPanelIndex >= 0)
{
OpenedUIList[lastPanelIndex].PanelInstance.ShowUI();
}
}
}
OpenedUIList.Remove(uiInfo);
}
}
public class UIInfo
{
public string UIName;
public bool IsCache;
public bool IsDynamic;//是否是动态UI,例如屏幕外导航
public UIOverlayMode OverlayMode;
public UIControlMode ControlMode;
public UILayer Layer;
public UIBasePanel PanelInstance;
public bool IsOpen = false;
}
public enum UILayer
{
Back,
Mid,
Front
}
public enum UIOverlayMode
{
NewPanel,//独立的UI界面,同时只能显示一个,例如活动页面,点击活动后跳转的页面
Additive//叠加模式,例如弹窗
}
public enum UIControlMode
{
System,//完全由代码控制UI的开关,例如物品不足提示UI
Player//玩家可以主动开启或关闭,点击按钮或者使用快捷键开关
}
public class PathUtil
{
public static string GetUIPath(string uiName)
{
return $"UI/{uiName}";
}
public static string GetUIDataPath()
{
return "Data/UIInfos";
}
}
关键字段以及方法
使用Resources动态加载UI资源并添加对应的UI脚本,使用PathUtil存储对应的资源路径
private Dictionary<string, UIInfo> UIInfos;存放每一个UI的配置信息
private List<UIInfo> OpenedUIList;存放每一个打开的UI
public UIBasePanel Open(string uiName);打开指定名称的UI,调用对应UI的OnOpen方法,如果UI没有被缓存,先实例化出来,调用OnInit函数后再调用OnOpen方法
private void OnUIOpen(UIInfo uiInfo);当打开UI时调用,如果打开的UI的类型是NewPanel,那么调用上一个打开的NewPanel类型的UI的HideUI方法
public void Close(string uiName);关闭指定名称的UI
private void OnUIClose(UIInfo uiInfo);//当关闭UI时调用,如果关闭的UI的类型是NewPanel,那么调用上一个NewPanel类型的ShowUI方法
public bool CloseRecent();//关闭最近打开的可以由玩家控制的面板,用于使用快捷键关闭UI
预制体规范
UIRoot和Back,Mid,Front为空物体,DynamicUIRoot和StaticUIRoot为Canvas
每一个UI预制体下面都有一个Root节点,所有的UI元素都放到Root节点下便于管理
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)