Unity UI框架总结

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 subChunks;

public void InitAsset(UIView view)

{

canvasGroup = view.gameObject.GetComponent();

InternalInit(view);

}

private void InternalInit(UIView view)

{

var subViews = view.gameObject.GetComponents();

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

{

public Dictionary render2Tran;

private Dictionary uiChunks = new Dictionary();

private int uiPriority = 0;

public UIChunk ShowUI(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(path); // 一般采用异步来做,这里简单演示,采用同步来做

var transform = render2Tran[view.layerType];

view.transform.SetParent(transform, false);

chunk.InitAsset(view);

chunk.Show();

}

public void RefreshLayerOrder(LayerType type)

{

List chunks = new List();

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 chunks = new List();

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 ConfigsMap = new Dictionary()

{

[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 ViewMaps = new Dictionary()

{

[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();

return _rectTransform;

}

}

}

}

UIMainChunk.cs UIMainParam.cs UIMainView.cs

UISubAChunk.cs UISubAParam.cs UISubAView.cs

都是继承对应结构的空类,就不展示了,后续我有时间将代码优化上传到GitHub。

相关文章

bat365官网登录下载
国际足联对阿根廷队世界杯决赛中的不当行为进行调查
bat365官网登录下载
PSP3000如何升级官方5.03

PSP3000如何升级官方5.03

📅 10-08 👀 5170