如果|

陈侠云

园龄:2年10个月粉丝:1关注:1

Unity UI框架总结

前言

目前国内手游的开发过程中,大部分业务玩法都是围绕着UI进行的。一个玩法业务不管是大型还是小型,UI上能占用40%-60%的工作量,不过当然也与玩法类型也有关系,玩法越偏3D,UI占有率越低,玩法越偏2D,UI占有率就越高,甚至能达到100%。博主作为一个3年多工作经验的U3D小白,日常工作大部分都跟UI息息相关,积累了不少的工作经验。趁现在空闲时间比较多,整理一下对UI框架的理解。

一个好用的UI框架在博主看来起码要能支持到这几种功能:

  1. 支持UI的Init,Enable,Show,Disable,Destroy,Visible, Update这些基本事件回调的编写,我们的日常工作大多也是基于这些回调进行功能上的开发。
  2. 支持多个不同的层次栈,多个栈可以处理不同层级需求。新界面一定处理层次栈节点的末尾。
  3. 支持多个不同的显示栈,每个显示栈只能有一个UI进行显示。控制打开新界面时,是否隐藏当前显示的界面。关闭新界面时,恢复对隐藏界面的显示。
  4. 尽量代码逻辑处理要做到同步,不要让业务人员去思考异步代码的编写,因为这还涉及异步资源的释放,会导致日常开发的困难。
  5. UI自己申请的资源要自己做到释放,比如在Enable有自动注册一些功能,例如事件监听,需要在Disable去主动的解绑,避免业务人员在开发过程还要大量思考内存泄露问题。
  6. 需要有定时销毁UI的功能,避免一些关闭的UI长时间占据内存。
  7. 支持界面UI嵌套逻辑,我们日常业务开发过程中,常常会碰到如下逻辑,一个主界面有2个页签,点击任意页签会显示不同的UI。我认为比较好的UI逻辑是,主界面为UI_A,嵌套2个子UI(UI_B,UI_C),点击左页签,会显示UI_B,隐藏UI_C;点击右页签,会显示UI_C,隐藏UI_B。
  8. 支持自定义参数传递,需要从父UI传递到子UI。在开发成就相关的功能时,常常需要我们跳转到相关的界面,并且还要在对应的界面进行一些展示处理。
  9. 同时打开多个UI时,要根据调用打开的接口的顺序按序显示UI,而非通过资源加载完成后的顺序去显示。
  10. UI框架要维护好已打开界面的缓存和释放,避免界面频繁重新加载,以及不能释放导致的内存泄露问题。

题外话,顺便说一下目前项目框架采用一些的设置:

  1. 采用UGUI框架。
  2. Root节点上Canvas,采用Screen Space - Camera的Render Mode,Root底下的层次栈节点不能选择Override Sorting,依赖本身的层次处理层级关系。
    image
  3. UI上不含有Canvas,所有UI默认为单例,不允许生成多个相同的UI界面。

缺点

当然同异步,同步一样,采用了同步思维去简化日常UI开发流程,必然也要迎接因为同步带来的大量性能损耗,以我们项目目前最大的一个预制体为例,大小为4835KB,我们在Editor下通过Profile性能检测工具去看一下首次打开这个预制体界面带来的性能消耗(不等同于实际运行环境,编辑器下是同步加载资源):
image
可以注意到这个Loading.ReadObject带来了非常高的CPU耗时和GC消耗,这个函数功能主要是将资源从硬盘上加载到内存当中。然后我们再来看下一帧:
image
总耗时是1233.76ms,关对预制体进行实例化就占去了一半的CPU耗时,另外一半是业务层对UI的初始化带来的消耗,这里不演示。
所以说,如果采用同步的思维去做UI框架,对于一些性能敏感的界面,还是有必要再进一步的进行优化,避免加载带来的大量CPU的消耗,造成卡顿。

UI配置

在开发UI时,有一些关键配置我们可以单独配置预制体上,方便我们进行开发调整,比如:

  1. UI名称
  2. 所处层次栈名称
  3. 所处显示栈名称(可以不设置)
  4. 打开后是否显示黑底

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节点。

代码示例

接下来,我通过代码的方式实现一下我上面说的大部分功能,实现过程演示为主,代码非常粗糙,有兴趣的朋友可以自己改写下进行优化。

代码结构:
image

场景结构:
image

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 中国大陆许可协议进行许可。

posted @   陈侠云  阅读(43)  评论(0编辑  收藏  举报
//雪花飘落效果
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起