前言
前面的文章讲述了关于Unity下资源的打包、加载以及打包工具的设计。从本文开始介绍UI的交互设计。游戏开发中存在很多的UI界面,虽然UGUI本身通过UI节点的位置对显示层级做了处理,但是实际开发中存在部分界面内还存在3D模型的展示、特效的展示,如果设计不当就会存在特效显示穿透的问题,开关界面是遮挡问题。本文就结合之前基于Addressable的资源加载,缓存池等来构建UI界面交互逻辑。本文主要介绍UI窗口的层级控制,
程序设计思路
UI交互部分核心的设计理念:动态的刷新UI、特效等层级。保证每一层之间的显示相互不会出现穿插。程序的流程图大致如下图所示:
由于这部UI交互设计的代码亮较大,这里讲比较核心的一些代码片段贴出,后续Unity通用框架搭建这一系列文章梳理完后会上传完整的Demo工程
- UIStack:管理每一层窗口下的UI窗口
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 维护每一层的窗口信息
/// 刷新当前堆栈层级信息
/// </summary>
public class UIStack:MonoBehaviour
{
/// <summary>
/// 层级节点
/// </summary>
public RectTransform Root;
[SerializeField]
private Canvas UICanvas;
/// <summary>
/// 记录当前属于第几级窗口
/// </summary>
public int Level = 0;
private List<UIBase> nodes = new List<UIBase>();
/// <summary>
/// 判断当前层级是否没有窗口
/// </summary>
public bool Visible {
get
{
bool visible = false;
foreach (var info in nodes)
{
if (info.IsShow)
{
visible = true;
}
}
return visible;
}
set
{
this.Root.gameObject.SetActive(value);
}
}
/// <summary>
/// 静态方法用于创建窗口缓存堆栈
/// </summary>
/// <param name="level"></param>
/// <returns></returns>
public static UIStack Create(int level)
{
GameObject obj = new GameObject("Window Stack: " + level.ToString());
var stack=obj.AddComponent<UIStack>();
stack.Init(level);
return stack;
}
public void Init(int level)
{
Level = level;
RectTransform stackNode = this.gameObject.AddComponent<RectTransform>();
stackNode.name = "Window Stack: " + level.ToString();
stackNode.transform.SetParent(UIManager.Instance.Root);
stackNode.gameObject.layer = UIManager.Instance.Root.gameObject.layer;
stackNode.localScale = Vector3.one;
stackNode.localPosition = Vector3.zero;
stackNode.localRotation = Quaternion.identity;
stackNode.anchorMin = Vector2.zero;
stackNode.anchorMax = Vector3.one;
stackNode.offsetMin = Vector2.zero;
stackNode.offsetMax = Vector2.zero;
Canvas canvas = stackNode.gameObject.AddComponent<Canvas>();
canvas.overrideSorting = true;
UnityEngine.UI.GraphicRaycaster graphic = stackNode.gameObject.AddComponent<UnityEngine.UI.GraphicRaycaster>();
UICanvas = canvas;
Root = stackNode;
}
/// <summary>
/// 复用Stack避免重复的创建和销毁
/// </summary>
/// <param name="level"></param>
public void ResetUIStack(int level)
{
this.Root.name = "Window Stack: " + level.ToString();
this.Level = level;
this.nodes.Clear();
this.Root.gameObject.SetActive(true);
this.Root.transform.SetAsLastSibling();
}
/// <summary>
/// 刷新当前层级下的窗口信息
/// </summary>
/// <param name="sortOrder"></param>
/// <returns></returns>
public int UpdateOrder(int sortOrder)
{
Helper.Log("Stack: " + this.Level + " depth: " + sortOrder * UIManager.DepthSpace);
this.UICanvas.sortingOrder = sortOrder * UIManager.DepthSpace;
foreach (var info in this.nodes)
{
info.SortOrder = sortOrder++;
}
return sortOrder;
}
/// <summary>
/// 压入新开的窗口
/// </summary>
/// <param name="obj"></param>
public void Push(UIBase obj)
{
obj.transform.SetParent(this.Root);
obj.transform.SetAsLastSibling();
obj.Level = this.Level;
obj.stack = this;
var prefab = AssetsManager.Instance.GetTemplete(obj.name);
if (prefab != null)
{
obj.transform.localPosition = prefab.transform.localPosition;
obj.transform.localScale = prefab.transform.localScale;
obj.transform.localRotation = prefab.transform.localRotation;
var rect = obj.GetComponent<RectTransform>();
var prefabRect = prefab.GetComponent<RectTransform>();
if (rect != null)
{
rect.localScale = prefabRect.localScale;
rect.localPosition = prefabRect.localPosition;
rect.localRotation = prefabRect.localRotation;
rect.anchorMin = prefabRect.anchorMin;
rect.anchorMax = prefabRect.anchorMax;
rect.offsetMin = prefabRect.offsetMin;
rect.offsetMax = prefabRect.offsetMax;
}
}
if (this.nodes.Contains(obj))
{
this.nodes.Remove(obj);
}
this.nodes.Add(obj);
this.Visible = true;
}
/// <summary>
/// 弹出最后打开的窗口
/// </summary>
/// <returns></returns>
public UIBase Pop()
{
if (this.nodes.Count > 0)
{
var pop = this.nodes[this.nodes.Count - 1];
this.nodes.Remove(pop);
return pop;
}
return null;
}
/// <summary>
/// 移除指定的窗口
/// </summary>
/// <param name="obj"></param>
public void Remove(UIBase obj)
{
if (this.nodes.Contains(obj))
{
this.nodes.Remove(obj);
}
///界面全部移除自动关闭
Visible = Visible;
if (this.nodes.Count <= 0)
{
if (UIManager.Instance.Level == this.Level)
{
UIManager.Instance.Level--;
}
}
}
}
- SortOrderInfo:记录初始层级信息
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public struct CanvasInfo
{
public Canvas UICanvas;
public int SortOrder;
}
public struct RendererInfo
{
public Renderer Renderer;
public int SortOrder;
}
public struct SpriteMaskInfo
{
public SpriteMask mask { get; set; }
public int FrontSortOrder { get; set; }
public int BackSortOrder { get; set; }
}
public class SortOrderInfo:ISortOrder
{
private GameObject root;
private List<RendererInfo> renderers = new List<RendererInfo>();
private List<CanvasInfo> canvas = new List<CanvasInfo>();
private List<SpriteMaskInfo> spriteMasks = new List<SpriteMaskInfo>();
private bool isInit = false;
public SortOrderInfo(GameObject obj)
{
root = obj;
renderers.Clear();
canvas.Clear();
spriteMasks.Clear();
init();
}
private void init()
{
if (isInit)
return;
isInit = true;
var list_canvas = this.root.GetComponentsInChildren<Canvas>(true);
for (int i = 0; i < list_canvas.Length; i++)
{
CanvasInfo info = new CanvasInfo();
info.UICanvas = list_canvas[i];
info.SortOrder = list_canvas[i].sortingOrder;
canvas.Add(info);
}
var list_renderer = this.root.GetComponentsInChildren<Renderer>(true);
for (int i = 0; i < list_renderer.Length; i++)
{
RendererInfo info = new RendererInfo();
info.Renderer = list_renderer[i];
info.SortOrder = list_renderer[i].sortingOrder;
renderers.Add(info);
}
var list_spriteMasks = this.root.GetComponentsInChildren<SpriteMask>(true);
for (int i = 0; i < list_spriteMasks.Length; i++)
{
SpriteMaskInfo info = new SpriteMaskInfo();
info.mask = list_spriteMasks[i];
info.FrontSortOrder = list_spriteMasks[i].frontSortingOrder;
info.BackSortOrder = list_spriteMasks[i].backSortingOrder;
spriteMasks.Add(info);
}
}
/// <summary>
/// 刷新显示层级
/// sortOrder为零时恢复层级为初始状态
/// </summary>
/// <param name="sortOrder"></param>
public void UpdateSortOrder(int sortOrder=0)
{
init();
foreach (var info in canvas)
{
info.UICanvas.sortingOrder = info.SortOrder + sortOrder * UIManager.DepthSpace;
}
foreach (var info in renderers)
{
info.Renderer.sortingOrder = info.SortOrder + sortOrder * UIManager.DepthSpace;
}
foreach (var info in spriteMasks)
{
info.mask.frontSortingOrder = info.FrontSortOrder + sortOrder * UIManager.DepthSpace;
info.mask.backSortingOrder = info.mask.frontSortingOrder - 1;
}
}
}
- UIManager:核心的UI逻辑控制
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// UI 加载管理类
/// </summary>
public class UIManager : Singleton<UIManager>
{
public static int DepthSpace=100;
/// <summary>
/// 缓存窗口信息
/// </summary>
private Hashtable caches;
private bool isInit = false;
public Transform Root;
/// <summary>
/// 当前最高层级
/// </summary>
public int Level=0;
/// <summary>
/// 记录各级窗口堆栈
/// </summary>
private SortedDictionary<int, UIStack> stacks;
private int order;
/// <summary>
/// 是否需要刷新所有的UI层级
/// </summary>
public bool updateSortOrder
{
set
{
if (value)
{
order = 1;
foreach (var stack in this.stacks)
{
order = stack.Value.UpdateOrder(order);
}
this.updateSortOrder = false;
}
}
}
/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
public override bool Init()
{
if (isInit)
return true;
isInit = true;
this.Root = GameObject.Find("Canvas").transform;
this.caches = new Hashtable();
stacks = new SortedDictionary<int, UIStack>();
this.Level = 0;
return true;
}
public UIStack GetUIStack(int level)
{
UIStack stack;
if (!this.stacks.TryGetValue(level, out stack))
{
//遍历已有的窗口栈,重复使用已有空闲的窗口栈
foreach (var info in this.stacks)
{
if (!info.Value.Visible)
{
if (info.Value.Root.transform.childCount == 0)
{
stack = info.Value;
this.stacks.Remove(info.Key);
this.stacks.Add(level, stack);
stack.ResetUIStack(level);
return stack;
}
}
}
stack = UIStack.Create(level);
this.stacks.Add(level, stack);
}
return stack;
}
/// <summary>
/// 打开窗口,全局唯一
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="args"></param>
public T Show<T>(params object[] args) where T : UIBase
{
return Show(typeof(T).ToString(), Level + 1,args) as T;
}
public T Show<T>(int level, params object[] args) where T : UIBase
{
return Show(typeof(T).ToString(), level, args) as T;
}
public UIBase Show(string name,int level, params object[] args)
{
Level = level;
UIBase window = null;
if (this.caches.ContainsKey(name))
{
window = this.caches[name] as UIBase;
window.RemoveFromUIStack();
}
else
{
var obj = AssetsManager.Instance.Instantiate(name);
window = obj.GetComponent<UIBase>();
if (!this.caches.ContainsKey(name))
{
this.caches.Add(name, window);
}
}
var stack = this.GetUIStack(Level);
stack.Push(window);
window.gameObject.SetActive(false);
this.updateSortOrder = true;
window.Init();
window.Show(args);
return window;
}
public void Hide<T>(params object[] args) where T : UIBase
{
Hide(typeof(T).ToString(), args);
}
public void Hide(string name, params object[] args)
{
var window = Get(name);
if (window != null)
{
window.Hide(args);
}
}
/// <summary>
/// 打开窗口,允许存在多个,需要手动关闭
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="args"></param>
public T Open<T>(params object[] args) where T: UIBase
{
return Open(typeof(T).ToString(), Level + 1, args) as T;
}
public T Open<T>(int level, params object[] args) where T : UIBase
{
return Open(typeof(T).ToString(), level, args) as T;
}
public UIBase Open(string name,int level, params object[] args)
{
Level = level;
var obj = AssetsManager.Instance.Instantiate(name);
var window = obj.GetComponent<UIBase>();
var stack = this.GetUIStack(Level);
stack.Push(window);
window.gameObject.SetActive(false);
this.updateSortOrder = true;
window.Init();
window.Show(args);
return window;
}
/// <summary>
/// 获取指定窗口
/// </summary>
/// <typeparam name="T">窗口类型</typeparam>
/// <returns>已经在缓存中的窗口</returns>
public T Get<T>() where T : UIBase
{
T window = null;
if (this.caches.ContainsKey(typeof(T).ToString()))
{
window = this.caches[typeof(T).ToString()] as T;
}
return window;
}
public UIBase Get(string name)
{
UIBase window = null;
if (this.caches.ContainsKey(name))
{
window = this.caches[name] as UIBase;
}
return window;
}
/// <summary>
/// 移除缓存
/// </summary>
/// <param name="obj"></param>
public void RemoveCaches(UIBase obj)
{
if (this.caches.ContainsKey(obj.name))
{
this.caches.Remove(obj.name);
}
}
}
结尾语
很多程序猿都有一个不好习惯,一股脑的将UI界面的一些交互逻辑和显示逻辑都写到UIWindow的脚本代码中。这样做虽然前期开发便捷,但是随着功能的开发会逐渐使窗口的代码变得十分臃肿,在整改功能逻辑时十分困难,不管时开发者自己还是旁人再二次开发时候都会感觉很头疼。因此程序再设计上需要考虑到设计模式的理念单一职责原则,将脚本功能按需划分,最大程度上的进行程序解耦。后续文章也会逐步完善这一设计理念。