Unity通用框架搭建(九)—— UI交互设计之动态层级刷新

前言

前面的文章讲述了关于Unity下资源的打包、加载以及打包工具的设计。从本文开始介绍UI的交互设计。游戏开发中存在很多的UI界面,虽然UGUI本身通过UI节点的位置对显示层级做了处理,但是实际开发中存在部分界面内还存在3D模型的展示、特效的展示,如果设计不当就会存在特效显示穿透的问题,开关界面是遮挡问题。本文就结合之前基于Addressable的资源加载,缓存池等来构建UI界面交互逻辑。本文主要介绍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的脚本代码中。这样做虽然前期开发便捷,但是随着功能的开发会逐渐使窗口的代码变得十分臃肿,在整改功能逻辑时十分困难,不管时开发者自己还是旁人再二次开发时候都会感觉很头疼。因此程序再设计上需要考虑到设计模式的理念单一职责原则,将脚本功能按需划分,最大程度上的进行程序解耦。后续文章也会逐步完善这一设计理念。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值