Unity UI框架总结
前言
目前国内手游的开发过程中,大部分业务玩法都是围绕着UI进行的。一个玩法业务不管是大型还是小型,UI上能占用40%-60%的工作量,不过当然也与玩法类型也有关系,玩法越偏3D,UI占有率越低,玩法越偏2D,UI占有率就越高,甚至能达到100%。博主作为一个3年多工作经验的U3D小白,日常工作大部分都跟UI息息相关,积累了不少的工作经验。趁现在空闲时间比较多,整理一下对UI框架的理解。
一个好用的UI框架在博主看来起码要能支持到这几种功能:
- 支持UI的Init,Enable,Show,Disable,Destroy,Visible, Update这些基本事件回调的编写,我们的日常工作大多也是基于这些回调进行功能上的开发。
- 支持多个不同的层次栈,多个栈可以处理不同层级需求。新界面一定处理层次栈节点的末尾。
- 支持多个不同的显示栈,每个显示栈只能有一个UI进行显示。控制打开新界面时,是否隐藏当前显示的界面。关闭新界面时,恢复对隐藏界面的显示。
- 尽量代码逻辑处理要做到同步,不要让业务人员去思考异步代码的编写,因为这还涉及异步资源的释放,会导致日常开发的困难。
- UI自己申请的资源要自己做到释放,比如在Enable有自动注册一些功能,例如事件监听,需要在Disable去主动的解绑,避免业务人员在开发过程还要大量思考内存泄露问题。
- 需要有定时销毁UI的功能,避免一些关闭的UI长时间占据内存。
- 支持界面UI嵌套逻辑,我们日常业务开发过程中,常常会碰到如下逻辑,一个主界面有2个页签,点击任意页签会显示不同的UI。我认为比较好的UI逻辑是,主界面为UI_A,嵌套2个子UI(UI_B,UI_C),点击左页签,会显示UI_B,隐藏UI_C;点击右页签,会显示UI_C,隐藏UI_B。
- 支持自定义参数传递,需要从父UI传递到子UI。在开发成就相关的功能时,常常需要我们跳转到相关的界面,并且还要在对应的界面进行一些展示处理。
- 同时打开多个UI时,要根据调用打开的接口的顺序按序显示UI,而非通过资源加载完成后的顺序去显示。
- UI框架要维护好已打开界面的缓存和释放,避免界面频繁重新加载,以及不能释放导致的内存泄露问题。
题外话,顺便说一下目前项目框架采用一些的设置:
- 采用UGUI框架。
- Root节点上Canvas,采用Screen Space - Camera的Render Mode,Root底下的层次栈节点不能选择Override Sorting,依赖本身的层次处理层级关系。
- UI上不含有Canvas,所有UI默认为单例,不允许生成多个相同的UI界面。
缺点
当然同异步,同步一样,采用了同步思维去简化日常UI开发流程,必然也要迎接因为同步带来的大量性能损耗,以我们项目目前最大的一个预制体为例,大小为4835KB,我们在Editor下通过Profile性能检测工具去看一下首次打开这个预制体界面带来的性能消耗(不等同于实际运行环境,编辑器下是同步加载资源):
可以注意到这个Loading.ReadObject带来了非常高的CPU耗时和GC消耗,这个函数功能主要是将资源从硬盘上加载到内存当中。然后我们再来看下一帧:
总耗时是1233.76ms,关对预制体进行实例化就占去了一半的CPU耗时,另外一半是业务层对UI的初始化带来的消耗,这里不演示。
所以说,如果采用同步的思维去做UI框架,对于一些性能敏感的界面,还是有必要再进一步的进行优化,避免加载带来的大量CPU的消耗,造成卡顿。
UI配置
在开发UI时,有一些关键配置我们可以单独配置预制体上,方便我们进行开发调整,比如:
- UI名称
- 所处层次栈名称
- 所处显示栈名称(可以不设置)
- 打开后是否显示黑底
UI接口
接下来列举一些工作常用的UI接口,以及对这些接口起码达到的期望。
ShowUI
函数:ShowUI(string key, Param param = null)
参数:
- key:UI名称
- param:对应UI界面的传入参数
功能:
- key不区分该UI是子UI还是父UI,假如子UI,自动去寻找父UI,并实际上调用父UI的打开接口。
- param需要自动实例化父UI的param(可以通过反射的方式实例化),并在父param中存储当前需要打开的子UI对象名称,方便父UI做Show逻辑时知道当前要显示哪个子UI界面。
- 如果UI界面资源未加载,进行加载,加载完成进行父UI、子UI的初始化;
- 根据打开UI时设置的showPriority,将该UI节点设置到层次栈的末尾,保证在同层次栈内该UI一定能显示出来。隐藏跟该UI同显示栈的其他节点(Visibele)。
CloseUI
函数:CloseUI(string key)
参数:
- key:UI名称
功能: - 隐藏该UI节点,并显示出跟该UI同显示栈的下一个UI节点。
代码示例
接下来,我通过代码的方式实现一下我上面说的大部分功能,实现过程演示为主,代码非常粗糙,有兴趣的朋友可以自己改写下进行优化。
代码结构:
场景结构:
UIChunk.cs
using System.Collections.Generic;
using UnityEngine;
namespace XiaYun.UI
{
public abstract class UIParam
{
public UIParam subParam;
}
public abstract class UIChunk
{
// 通用数据
public UIView view;
public CanvasGroup canvasGroup;
public int showPriority;
public bool isAssetInit => view != null;
public bool isShow => isAssetInit && view.gameObject.activeSelf;
public bool isRoot => UIConfigs.ConfigsMap[GetType()].parent == null;
// 基本数据
private List<UIChunk> subChunks;
public void InitAsset(UIView view)
{
canvasGroup = view.gameObject.GetComponent<CanvasGroup>();
InternalInit(view);
}
private void InternalInit(UIView view)
{
var subViews = view.gameObject.GetComponents<UIView>();
foreach (var sub in subViews)
{
var configs = UIConfigs.ViewMaps[sub.GetType()];
if (configs.parent == GetType())
{
var chunk = System.Activator.CreateInstance(configs.chunkType) as UIChunk;
subChunks.Add(chunk);
chunk.InternalInit(sub);
}
}
OnInit();
}
protected virtual void OnInit(){}
public void Show()
{
view.gameObject.SetActive(true);
if (isRoot)
{
UIManager.Instance.RefreshLayerOrder(view.layerType);
UIManager.Instance.RefreshRenderOrder(view.renderStack);
}
OnShow();
foreach (var sub in subChunks)
{
sub.OnShow();
}
}
protected virtual void OnShow(){}
public void SetVisible(bool visible)
{
canvasGroup.alpha = visible ? 1 : 0;
}
public void Close()
{
view.gameObject.SetActive(false);
if (isRoot)
{
UIManager.Instance.RefreshLayerOrder(view.layerType);
UIManager.Instance.RefreshRenderOrder(view.renderStack);
}
OnClose();
foreach (var sub in subChunks)
{
sub.OnClose();
}
}
protected void OnClose(){}
}
}
UIManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace XiaYun.UI
{
public class UIManager : ComponentSerializedSingleton<UIManager>
{
public Dictionary<LayerType, Transform> render2Tran;
private Dictionary<Type, UIChunk> uiChunks = new Dictionary<Type, UIChunk>();
private int uiPriority = 0;
public UIChunk ShowUI<T>(UIParam param = null)
{
var type = typeof(T);
// 找到根节点
UIConfigs.ConfigsMap.TryGetValue(type, out var configs);
while (configs.parent != null)
{
UIConfigs.ConfigsMap.TryGetValue(configs.parent, out var temp);
configs = temp;
// 生成根节点Param
if (param != null)
{
var parentParam = System.Activator.CreateInstance(configs.paramType) as UIParam;
parentParam.subParam = param;
param = parentParam;
}
}
// 如果有缓存
if (uiChunks.TryGetValue(configs.chunkType, out var uiChunk))
{
uiChunk.showPriority = ++uiPriority;
uiChunk.Show();
return uiChunk;
}
uiChunk = System.Activator.CreateInstance(configs.chunkType) as UIChunk;
uiChunk.showPriority = ++uiPriority;
uiChunks.Add(uiChunk.GetType(), uiChunk);
LoadAsset(uiChunk, configs.resPath);
return uiChunk;
}
private void LoadAsset(UIChunk chunk, string path)
{
var view = Resources.Load<UIView>(path); // 一般采用异步来做,这里简单演示,采用同步来做
var transform = render2Tran[view.layerType];
view.transform.SetParent(transform, false);
chunk.InitAsset(view);
chunk.Show();
}
public void RefreshLayerOrder(LayerType type)
{
List<UIChunk> chunks = new List<UIChunk>();
foreach (var kvp in uiChunks)
{
if (kvp.Value.isShow && kvp.Value.view.layerType == type)
{
chunks.Add(kvp.Value);
}
}
chunks.Sort((a, b) => a.showPriority.CompareTo(b.showPriority));
for (int i = 0; i < chunks.Count; i++)
{
var chunk = chunks[i];
chunk.view.rectTransform.SetAsLastSibling();
}
}
public void RefreshRenderOrder(RenderType type)
{
List<UIChunk> chunks = new List<UIChunk>();
foreach (var kvp in uiChunks)
{
if (kvp.Value.isShow && kvp.Value.view.renderStack == type)
{
chunks.Add(kvp.Value);
}
}
chunks.Sort((a, b) => a.showPriority.CompareTo(b.showPriority));
for (int i = chunks.Count - 1; i >= 0; i--)
{
var chunk = chunks[i];
chunk.SetVisible(i == chunks.Count - 1);
}
}
}
}
UIType
namespace XiaYun.UI
{
// 显示类型
public enum RenderType
{
None,
Main
}
// 层级类型
public enum LayerType
{
None,
Bottom,
Top,
Login
}
}
UIConfigs.cs
using System;
using System.Collections.Generic;
namespace XiaYun.UI
{
public class UIConfigs
{
public class Configs
{
public Type chunkType;
public Type paramType;
public Type parent;
public string resPath;
}
public static Dictionary<Type, Configs> ConfigsMap = new Dictionary<Type, Configs>()
{
[typeof(UIMainChunk)] = new Configs()
{
chunkType = typeof(UIMainChunk),
paramType = typeof(UIMainParam),
parent = null,
resPath = "Assets/UI框架/UI/Res/Main"
},
[typeof(UISubAChunk)] = new Configs()
{
chunkType = typeof(UISubAChunk),
paramType = typeof(UISubAParam),
parent = typeof(UIMainChunk),
resPath = "Assets/UI框架/UI/Res/Main"
}
};
public static Dictionary<Type, Configs> ViewMaps = new Dictionary<Type, Configs>()
{
[typeof(UIMainView)] = new Configs()
{
chunkType = typeof(UIMainChunk),
paramType = typeof(UIMainParam),
parent = null,
resPath = "Assets/UI框架/UI/Res/Main"
},
[typeof(UISubAView)] = new Configs()
{
chunkType = typeof(UISubAChunk),
paramType = typeof(UISubAParam),
parent = typeof(UIMainChunk),
resPath = "Assets/UI框架/UI/Res/Main"
}
};
}
}
UIView.cs
using UnityEngine;
namespace XiaYun.UI
{
public abstract class UIView : MonoBehaviour
{
public RenderType renderStack;
public LayerType layerType;
private RectTransform _rectTransform;
public RectTransform rectTransform
{
get
{
if (_rectTransform != null)
{
return _rectTransform;
}
_rectTransform = GetComponent<RectTransform>();
return _rectTransform;
}
}
}
}
UIMainChunk.cs UIMainParam.cs UIMainView.cs
UISubAChunk.cs UISubAParam.cs UISubAView.cs
都是继承对应结构的空类,就不展示了,后续我有时间将代码优化上传到GitHub。
本文作者:陈侠云
本文链接:https://www.cnblogs.com/chenxiayun/p/18734105
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步