用于管理Unity中UGUI的工具系统UISystem
一、简介
UISystem是由自己总结的一些便于管理UI工具,并将他集成到了QFrameWork框架中。一般来说采用栈结构先进后出管理ui面板会好很多,但经过一个项目后自我感觉不是很好用(因为我的项目的需求并不是游戏向,不需要从顶部开始操作)。由于它采用栈结构,在一些操作的时候不是很方便(虽然可以自己去添加对应功能)但在灵活性上我选择了双向链表,当然数据结构能满足你自己的想法就好,灵活变通即可,最后将他集成到了框架并写了配套编辑器使用
这里只是觉得使用起来还算方便(毕竟也没接触过太多ui管理),所以分享给大家。
QFrameWork地址下文已给出。
二、QFrameWork框架
框架地址:https://qframework.cn/qf
QFrameWork包含了UI,音频等内容,用于管理UI为UIKit,由于个人使用习惯故将自己的ui管理加入给自己使用。需要更详细关于QFrameWork的内容请到上方地址查阅。
三、编辑器
融入了QFrameWork的UIKit一些我觉得好用的知识点,比如比较麻烦的每个Panel都有自己的脚本去管理,这里我也写了适合自己的编辑器去帮助创建UI,自己创建脚本以及赋上和存储脚本以及预制体等功能,也只是为了方便一下,仅此而已。写完了在想有没有一个插件可以上传了ui图片后自动生成UI的层级目录,并且将上面我需要的内容都自动创建存储,所以自己又去写了一个,但是我觉得用处不大(纯粹闹着玩),不过后续想在上传一个ui设计图后自动切割对应类型并创建层级以及脚本,存储等内容,这样会节省大量创建UI的时间。具体实现在下一篇内容。
四、UISystem
1.思路:
通过将创建好的UIPanel预制体放入Resources文件夹下并存储到Panel路径字典中,以及一个存储已经创建好的UIPanel字典表示目前的Panel已经被创建并存在于Canvas(uiPanel的场景位置)下,在获取时先查询创建好字典中是否存在,不存在则动态加载路径字典中相应的预制体。然后提供方法去加载,销毁等操作Panel。
2.具体实现:
2.1双向链表
#region UISystem中双向链表节点
public class UIPanelNode
{
public BasePanel Panel; // 当前面板
public UIPanelNode Prev; // 指向前一个节点
public UIPanelNode Next; // 指向下一个节点
public UIPanelNode(BasePanel panel)
{
Panel = panel;
Prev = null;
Next = null;
}
}
#endregion
2.2UI面板类型
#region UITypeEnum
/// <summary>
/// 添加ui面板类型
/// </summary>
public enum UITypeEnum
{
System,//这里是Panel的类型
Tesk
}
#endregion
2.3Resources中存储的面板路径字典
#region UITypePathDictionary
/// <summary>
/// 更改Resources中存储的面板
/// </summary>
public class UITypePathDictionary
{
public Dictionary<UITypeEnum, string> UIPanelTypePath = new Dictionary<UITypeEnum, string>()
{
{UITypeEnum.System,"UiPanel/Systempanel"},
{UITypeEnum.Tesk,"UiPanel/Teskpanel"},
};
}
#endregion
这里使用枚举类型去存储,后续在使用的时候也可以通过枚举而不是输入具体名称,会方便很多
2.4BasePanel
#region BasePanel
public abstract class BasePanel : MonoBehaviour
{
private CanvasGroup canvasGroup;
/// <summary>
/// Panel打开时的操作,此时Panel打开
/// </summary>
public void OnOpen()
{
BaseForOpen();
ChildForOpen();
}
/// <summary>
/// Panel暂停时的操作,此时Panel不可被操作,但依然显示
/// </summary>
public void OnPause()
{
BaseForPause();
ChildForPause();
}
/// <summary>
/// Panel从暂停恢复时的操作,此时Panel可被操作,也显示
/// </summary>
public void OnResume()
{
BaseForResume();
ChildForResume();
}
/// <summary>
/// Panel关闭时的操作,此时Panel关闭
/// </summary>
public void OnClose()
{
BaseForClose();
ChildForClose();
}
/// <summary>
/// Panel隐藏时的操作,此时Panel不可被操作,也不显示,类似于Close,但Close是最终关闭,具体细节看ChildForHide与ChildForClose的区别
/// </summary>
public void OnHide()
{
BaseForHide();
ChildForHide();
}
/// <summary>
/// Panel从隐藏恢复为打开时的操作,此时Panel可被操作,也显示,类似于Resume,但Resume是为Pause提供的,具体细节看ChildForShowAgain与ChildForResume的区别
/// </summary>
public void OnShowAgain()
{
BaseForShowAgain();
ChildForShowAgain();
}
#region Open
/// <summary>
/// 父类默认逻辑
/// </summary>
private void BaseForOpen()
{
if (canvasGroup == null)
{
canvasGroup = GetComponent<CanvasGroup>();
}
if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
canvasGroup.blocksRaycasts = true;
canvasGroup.alpha = 1;
}
/// <summary>
/// 子类逻辑
/// </summary>
public abstract void ChildForOpen();
#endregion
#region Pause
/// <summary>
/// 父类默认逻辑
/// </summary>
private void BaseForPause()
{
canvasGroup.blocksRaycasts = false;
canvasGroup.alpha = 1;
}
/// <summary>
/// 子类逻辑
/// </summary>
public abstract void ChildForPause();
#endregion
#region Resume
/// <summary>
/// 父类默认逻辑
/// </summary>
private void BaseForResume()
{
canvasGroup.blocksRaycasts = true;
canvasGroup.alpha = 1;
}
/// <summary>
/// 子类逻辑
/// </summary>
public abstract void ChildForResume();
#endregion
#region Close
/// <summary>
/// 父类默认逻辑
/// </summary>
private void BaseForClose()
{
canvasGroup.blocksRaycasts = false;
canvasGroup.alpha = 0;
//这里不需要销毁物体,如果要销毁需要再存储Panel的字典中同步移除此Panel
//Destroy(gameObject); // 销毁当前面板的 GameObject
}
/// <summary>
/// 子类逻辑
/// </summary>
public abstract void ChildForClose();
#endregion
#region Hide
/// <summary>
/// 父类默认逻辑
/// </summary>
private void BaseForHide()
{
canvasGroup.blocksRaycasts = false;
canvasGroup.alpha = 0;
}
/// <summary>
/// 子类逻辑
/// </summary>
public abstract void ChildForHide();
#endregion
#region ShowAgain
/// <summary>
/// 父类默认逻辑
/// </summary>
private void BaseForShowAgain()
{
canvasGroup.blocksRaycasts = true;
canvasGroup.alpha = 1;
}
/// <summary>
/// 子类逻辑
/// </summary>
public abstract void ChildForShowAgain();
#endregion
}
#endregion
这里将子类需要实现的逻辑用抽象类实现,自己包含逻辑可以自己去更改,可以使用SetActive,也可以使用CanvasGroup等调整,根据自己的实际情况调整。
BasePanel包含六种方法,分别是1.打开2.暂停3.隐藏4.重新显示5.恢复6.关闭,具体实现可以在子类调整,或从BasePanel中调整。
2.5BasePanel的所有状态
#region BasePanel的所有状态
/// <summary>
/// BasePanel的所有状态(枚举类型)
/// </summary>
public enum SetNowTailPanelState
{
Open,
Pause,
Resume,
Close,
Hide,
ShowAgain,
NoSet
}
#endregion
这里多一个不设置,我也忘记为什么写了,大概是在处理的时候采用不设置的默认情况吧
2.6UISystem
#region UISystem
public interface IUISystem : ISystem
{
public void AddToBackPanel(UITypeEnum uITypeEnum);
public void RemovePanel(UITypeEnum uITypeEnum, SetNowTailPanelState SetNowPanelState);
public void SetPanelState(UITypeEnum uITypeEnum, SetNowTailPanelState SetNowPanelState);
public void CloseAllPanels();
}
public class UISystem : AbstractSystem, IUISystem, IController
{
private HashSet<BasePanel> panelSet; // 使用 HashSet 来存储面板 与双向链表同步
private Dictionary<UITypeEnum, BasePanel> panelDic;
private Dictionary<BasePanel, UIPanelNode> panelNodeCache; // 新增字典缓存面板与节点的映射 与双向链表同步
private UITypePathDictionary UITypePathDictionary;//存储ui路径的字典
private Canvas canvas;
private UIPanelNode head; // 链表头
private UIPanelNode tail; // 链表尾
private object lockObj = new object(); // 锁对象,用于确保线程安全
/// <summary>
/// 更改场景中的Canvas
/// </summary>
private Canvas Canvas
{
get
{
if (canvas == null)
{
canvas = GameObject.Find("Canvas").GetComponent<Canvas>();
}
return canvas;
}
}
public UISystem()
{
panelSet = new HashSet<BasePanel>();
panelDic = new Dictionary<UITypeEnum, BasePanel>();
panelNodeCache = new Dictionary<BasePanel, UIPanelNode>(); // 初始化缓存字典
UITypePathDictionary = new UITypePathDictionary();
head = tail = null;
}
/// <summary>
/// 在双向链表中添加指定Panel并执行当前链表尾Panel的指定状态(Panel只能从链表尾添加,遵循栈原理)
/// </summary>
/// <param name="uITypeEnum">指定Panel类型</param>
/// <param name="SetNowTailPanelState">链表尾Panel的指定状态</param>
public void AddToBackPanel(UITypeEnum uITypeEnum)
{
lock (lockObj)
{
BasePanel basePanel = GetPanel(uITypeEnum);
//判断链表中是否具有此Panel,如果有,则不添加,如果没有,则添加
if (FindPanel(basePanel))
{
Debug.LogError("链表中已有对应的面板: " + uITypeEnum.ToString());
return;
}
// 创建新节点
UIPanelNode newNode = new UIPanelNode(basePanel);
if (tail == null)
{
head = tail = newNode; // 如果链表为空,头尾指向新节点
}
else
{
tail.Next = newNode; // 追加到尾部
newNode.Prev = tail; // 设置前指针
tail = newNode; // 更新尾节点
}
// 给缓存中添加面板
panelSet.Add(basePanel);
// 缓存面板与节点的映射
panelNodeCache[basePanel] = newNode;
basePanel.OnOpen();// 打开面板
}
}
/// <summary>
/// 判断链表中是否具有此Panel,如果有,则不添加,如果没有,则添加
/// </summary>
/// <param name="uITypeEnum">指定Panel类型</param>
private bool FindPanel(BasePanel panelToRemove)
{
//UIPanelNode currentNode = head;
//while (currentNode != null)
//{
// if (currentNode.Panel == panelToRemove)
// {
// return true;
// }
// currentNode = currentNode.Next;
//}
Debug.LogWarning("没有找到对应的面板: " + panelToRemove.ToString());
//return false;
return panelSet.Contains(panelToRemove);
}
/// <summary>
/// 获取指定面板的链表节点(优化:通过缓存直接获取)
/// </summary>
private UIPanelNode GetNodeByPanel(BasePanel panel)
{
if (panelNodeCache.TryGetValue(panel, out UIPanelNode cachedNode))
{
return cachedNode;
}
// 如果缓存中没有,遍历链表查找(仅用于第一次查找)
UIPanelNode currentNode = head;
while (currentNode != null)
{
if (currentNode.Panel == panel)
{
panelNodeCache[panel] = currentNode; // 缓存查找到的节点
return currentNode;
}
currentNode = currentNode.Next;
}
return null; // 没找到
}
/// <summary>
/// 在双向链表中移除指定Panel并执行Panel的指定状态
/// </summary>
/// <param name="uITypeEnum">指定Panel类型</param>
/// <param name="SetNowPanelState">需要更改为SetNowPanelState状态</param>
public void RemovePanel(UITypeEnum uITypeEnum, SetNowTailPanelState SetNowPanelState)
{
lock (lockObj)
{
BasePanel panelToRemove = null;
panelDic.TryGetValue(uITypeEnum, out panelToRemove);
if (panelToRemove == null)
{
Debug.LogError("没有找到对应的面板: " + uITypeEnum.ToString());
return;
}
// 使用缓存或链表查找面板的节点
UIPanelNode currentNode = GetNodeByPanel(panelToRemove);
// 首先检查缓存中是否有此面板
if (currentNode != null)
{
// 从缓存中移除面板
panelSet.Remove(panelToRemove);
// 从缓存链表节点中移除面板
panelNodeCache.Remove(panelToRemove);
// 从链表中移除节点
if (currentNode.Prev != null)
{
currentNode.Prev.Next = currentNode.Next;
}
else
{
head = currentNode.Next; // 如果是头节点,更新头节点
}
if (currentNode.Next != null)
{
currentNode.Next.Prev = currentNode.Prev;
}
else
{
tail = currentNode.Prev; // 如果是尾节点,更新尾节点
}
//从双向链表中移除Panel时只需要考虑他关闭、隐藏的状态,如果考虑暂停状态(显示但不可操作),后续没法从链表中找到
switch (SetNowPanelState)
{
case SetNowTailPanelState.Close: currentNode.Panel.OnClose(); break;
case SetNowTailPanelState.Hide: currentNode.Panel.OnHide(); break;
default: currentNode.Panel.OnClose(); break;
}
// 断开当前节点的所有引用,防止内存泄漏
currentNode.Prev = null;
currentNode.Next = null;
currentNode = null; // 可以显式释放当前节点
}
else
{
Debug.LogError("没有找到对应的面板: " + uITypeEnum.ToString());
}
}
}
/// <summary>
/// 让某个Panel更改状态
/// </summary>
/// <param name="uITypeEnum">指定Panel类型</param>
/// <param name="SetNowPanelChangeState">需要更改为SetNowPanelChangeState状态</param>
public void SetPanelState(UITypeEnum uITypeEnum, SetNowTailPanelState SetNowPanelState)
{
lock (lockObj)
{
BasePanel panelToRemove = null;
panelDic.TryGetValue(uITypeEnum, out panelToRemove);
if (panelToRemove == null)
{
Debug.LogError("没有找到对应的面板: " + uITypeEnum.ToString());
return;
}
// 使用缓存或链表查找面板的节点
UIPanelNode nodeToUpdate = GetNodeByPanel(panelToRemove);
if (nodeToUpdate == null)
{
Debug.LogError("没有找到对应的面板节点: " + uITypeEnum.ToString());
return;
}
switch (SetNowPanelState)
{
case SetNowTailPanelState.Open: nodeToUpdate.Panel.OnOpen(); break;
case SetNowTailPanelState.Resume: nodeToUpdate.Panel.OnResume(); break;
case SetNowTailPanelState.Close: nodeToUpdate.Panel.OnClose(); break;
case SetNowTailPanelState.Pause: nodeToUpdate.Panel.OnPause(); break;
case SetNowTailPanelState.Hide: nodeToUpdate.Panel.OnHide(); break;
case SetNowTailPanelState.ShowAgain: nodeToUpdate.Panel.OnShowAgain(); break;
default: break;
}
}
}
/// <summary>
/// 断开所有双向链表,以及执行所有Panel的OnClose方法
/// </summary>
public void CloseAllPanels()
{
lock (lockObj)
{
UIPanelNode currentNode = head;
while (currentNode != null)
{
BasePanel basePanel = currentNode.Panel;
// 清空所有存储在 HashSet 中的面板
panelSet.Remove(basePanel);
// 清空所有存储在 链表节点字典 中的面板
panelNodeCache.Remove(basePanel);
basePanel.OnClose(); // 关闭当前面板
UIPanelNode nextNode = currentNode.Next; // 保存下一个节点的引用
// 从链表中断开当前节点
currentNode.Prev = null;
currentNode.Next = null;
currentNode = nextNode; // 移动到下一个节点
}
// 清空链表
head = null;
tail = null;
}
}
private BasePanel GetPanel(UITypeEnum uITypeEnum)
{
BasePanel basePanel;
panelDic.TryGetValue(uITypeEnum, out basePanel);
if (basePanel == null)
{
string path;
UITypePathDictionary.UIPanelTypePath.TryGetValue(uITypeEnum, out path);
if (path == null)
{
Debug.LogError(uITypeEnum.ToString() + "面板的存储路径是空的");
return null;
}
else
{
GameObject loadpanel = GameObject.Instantiate(Resources.Load<GameObject>(path));
loadpanel.transform.SetParent(Canvas.transform, false);
basePanel = loadpanel.GetComponent<BasePanel>();
panelDic.Add(uITypeEnum, basePanel);
return basePanel;
}
}
else
{
return basePanel;
}
}
protected override void OnInit()
{
}
}
#endregion
这只是实现并没有优化,可以自己优化一下,反正能跑就行了,毕竟我也没那么大项目
还有继承的IController接口与AbstractSystem,IUISystem是为了使用QFrameWork的this.getsystem,不使用QFrameWork只使用这个UISystem的话直接去掉就行了,单例使用即可,需要注意的是只能采用一种。具体原因下方说明。具体的使用方法下文写出。
具体的实现不难,看一下代码就看得懂,这里就不解释了,有不懂的可以私聊互相讨论。
五、使用说明
使用UISystem必须继承BasePanel,一个项目或者一个场景只能使用其中一种调用方法,
否则Instance或架构会造成实例不同而造成存储面板字典内容错误或链表内容不一、缓
存字典与缓存HashSet不同等问题。
1.通过QFrameWork使用
1.1.注册UISystem系统的类继承架构
1.2.需要使用UISystem的类继承接口IController并实现,在架构中返回注册系统。
1.3.代码实现:
public class RegisterMyUi : Architecture<RegisterMyUi>
{
protected override void Init()
{
RegisterSystem<IUISystem>(new UISystem());
}
}
public class YelloPanel : BasePanel, IController
{
void Start()
{
this.GetSystem<IUISystem>().RemovePanel(UITypeEnum.Tesk, SetNowTailPanelState.Close);
this.GetSystem<IUISystem>().AddToBackPanel(UITypeEnum.System);
}
public IArchitecture GetArchitecture()
{
return RegisterMyUi.Interface;
}
}
2.通过单例使用
2.1.继承BasePanel后直接使用
2.2.代码实现:
public class YelloPanel : BasePanel
{
void Start()
{
UISystem.OpenPanel(UITypeEnum.System);
}
}
3.方法说明
3.1.AddToBackPanel与OpenPanel
(1)AddToBackPanel在链表尾部插入一个新链表,调用需要传入一个UITypeEnum枚举类型参数。不提供在链表头以及链表中间插入新链表的方法(需要可以自己实现),因为虽然写的是链表,但遵循栈结构,目的是为了弥补栈不能访问中间Panel的不足,后续可以根据SetPanelState或者ChangePanelState方法在不删除顶部元素的时候改变其中一个Panel的状态。
(2)OpenPanel调用需要传入一个UITypeEnum枚举类型参数。
3.2.RemovePanel与ClosePanel
(1)RemovePanel从链表中删除一个链表,调用需要传入一个UITypeEnum枚举类型参数和一个SetNowTailPanelState枚举类型的状态。不提供单独在链表尾部删除链表的方法(需要可以自己实现),可以通过去遍历到链表尾部获取,因为面板虽然遵循栈结构但不保证不需要删除其中随意地方的,这里我不想删除的时候缓存栈结构面板去频繁的加载删除,所以使用双向链表。与SetPanelState和ChangePanelState最不同的地方就是RemovePanel与ClosePanel可以直接在链表中删除传入节点,然后才设置状态,而并不是直接改变但不删除链表,这样会造成链表内数据混乱,未成功管理面板数据
(2)ClosePanel调用需要传入一个UITypeEnum枚举类型参数和一个SetNowTailPanelState枚举类型的状态。
3.3.SetPanelState与ChangePanelState
(1)SetPanelState从链表中找到一个链表并改变他的状态,调用需要传入一个UITypeEnum枚举类型参数和一个SetNowTailPanelState枚举类型的状态。这是在不删除链表情况下的改变链表中Panel的状态
(2)ChangePanelState调用需要传入一个UITypeEnum枚举类型参数和一个SetNowTailPanelState枚举类型的状态。
3.4.CloseAllPanels与ClearPanels
(1)CloseAllPanels清空所有在链表中的面板并清空链表
(2)ClearPanels清空所有在链表中的面板并清空链表
由于篇幅太长编辑器内容下一篇进行写出。
本篇只做分享,如有改进的观点请私聊我,大家一同成长。