大家好我是开罗小8,这次我来介绍一下我设计的UI框架,先看一下实现的效果吧

实现效果:

 1.按下快捷键可以关闭最近打开的面板

2.同时只能打开一个UI,打开新的时会临时隐藏上一个UI,但弹窗类可以叠加显示的UI不受限制

3.使用Excel表格配置UI的显示模式,并且UI资源动态加载

由于本人经验有限,可能存在设计不合理的地方,如果大家有更好的方法欢迎在评论区指出


设计分析

在团队合作中,往往是策划负责设计UI如何进行显示,美术负责绘画以及拼接UI,程序负责功能的实现,期间策划和美术可能会频繁的修改,为了避免每次修改视觉效果都需要修改程序,因此我设计的UI框架在策划,美术修改完毕后直接替换对应的数据和资源即可。

策划配置表

UI配置表

在配置表中

IsCache:标记当前UI是否需要缓存

IsDynamic:表示当前是否是动态UI,即UI的大小,位置等属性是否一直在变化,如果是动态UI会放到另一个Canvas中做到动静分离

OverlayMode:表示叠加模式,如果是Additive表示当前UI可以同时显示

ControlMode:表示当前UI玩家是否可以主动关闭,如果是Player表示当前UI玩家可以主动关闭

Layer:表示UI的层级,数值越大越优先显示

如何将Excel表格导出Json格式并解析参考我的上一篇文章:使用Excel制作游戏配置表并导出Json和C#代码到Unity中使用-CSDN博客

美术资源

 美术资源只是已经拼好的UI界面的预制体,只包含UI基础组件(Image,Text,Button等),不包含任何脚本,预制体的名称需要和脚本的名称保持相同


代码设计

UI基类

UIBasePanel:每一个UI的基类,所有的UI都继承此脚本

public class UIBasePanel : MonoBehaviour
{
    protected UIInfo UIInfo;//存储UI的信息
    public bool IsShow { get; private set; }//当前UI是否正在展示
    protected GameObject PanelRoot;//当前UI的根节点

    public virtual void OnInit(UIInfo info)
    {
        UIInfo=info;
        PanelRoot = transform.Find("Root").gameObject;
    }
    public virtual void OnOpen()//开启UI,由UIManager进行控制
    {
        if (!IsShow)
        {
            ShowUI();
        }

    }
    public virtual void OnClose()//关闭UI,由UIManager进行控制
    {
        if (IsShow)
        {
            HideUI();
           
            if (!UIInfo.IsCache)
            {
                Destroy(gameObject);
            }
            
        }

    }
    public virtual void ShowUI()//展示UI,播放UI入场动画
    {
        IsShow = true;
        PanelRoot.SetActive(true);
        
    }
    public virtual void HideUI()//隐藏UI,播放UI关闭动画
    {
        IsShow = false;
        PanelRoot.SetActive(false);
    }

}

在OnInit函数中通过transform.Find动态获取对应的组件

UI管理器

UIManager:UI管理器,控制UI的开启与关闭等,使用单例模式

using System;
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
public class UIManager : Singleton<UIManager>
{
    private Transform UIRoot;
    private Transform StaticUIRoot;
    private Transform DynamicUIRoot;

    private Dictionary<string, UIInfo> UIInfos;
    private List<UIInfo> OpenedUIList;
    public UIManager()
    {
        Init();
    }
    private void Init()
    {
        UIInfos = new Dictionary<string, UIInfo>();
        OpenedUIList = new List<UIInfo>();
        CreateRoot();
        LoadUIData();
        Debug.Log("[UIManager Init]");
    }

    private void LoadUIData()
    {
        string jsonData=Resources.Load<TextAsset>(PathUtil.GetUIDataPath()).text;
        UIInfos = JsonConvert.DeserializeObject<Dictionary<string, UIInfo>>(jsonData);
    }

    private void CreateRoot()
    {
        GameObject uiRootRes = Resources.Load<GameObject>("UIRoot");
        if (uiRootRes == null)
        {
            Debug.LogError("未找到UIRoot资源");
        }
        UIRoot = GameObject.Instantiate(uiRootRes).transform;
        StaticUIRoot = UIRoot.transform.Find("StaticUIRoot");
        DynamicUIRoot = UIRoot.transform.Find("DynamicUIRoot");
        GameObject.DontDestroyOnLoad(UIRoot);
    }
    /// <summary>
    /// 打开指定UI面板
    /// </summary>
    /// <typeparam name="T">UI面板的类型</typeparam>
    /// <returns>对应的UI面板实例</returns>
    public UIBasePanel Open<T>()
    {
        string uiName = typeof(T).Name;
        return Open(uiName);
    }
    public UIBasePanel Open(string uiName)
    {
        if (UIInfos.ContainsKey(uiName))
        {
            UIInfo uiInfo = UIInfos[uiName];
            UIBasePanel panelInstance = uiInfo.PanelInstance;
            if (uiInfo.IsOpen)
            {
                Debug.LogWarning($"{uiName}面板已经打开,将不会再次调用OnOpen方法");

            }
            else
            {

                if (panelInstance == null)
                {
                    #region 实例化新的UI
                    GameObject uiRes = Resources.Load<GameObject>(PathUtil.GetUIPath(uiName));
                    if (uiRes == null)
                    {
                        Debug.LogError($"未找到对应路径下的UI资源:");
                        return null;
                    }
                    panelInstance = GameObject.Instantiate(uiRes).AddComponent(Type.GetType(uiName)) as UIBasePanel;
                    Transform targetTrans = null;
                    if (uiInfo.IsDynamic)
                    {
                        targetTrans = DynamicUIRoot.transform.Find(uiInfo.Layer.ToString());
                    }
                    else
                    {
                        targetTrans = StaticUIRoot.transform.Find(uiInfo.Layer.ToString());
                    }
                    if (targetTrans == null)
                    {
                        targetTrans = StaticUIRoot;
                        Debug.LogWarning("未找到UI对应的层级,请检查UIRoot每一个Canvas下是否包含所有UILayer的名称");
                    }
                    panelInstance.transform.SetParent(targetTrans, false);
                    
                    panelInstance.OnInit(uiInfo);
                    UIInfos[uiName].PanelInstance = panelInstance;
                    #endregion
                }
                panelInstance.OnOpen();
                panelInstance.transform.SetAsLastSibling();
                uiInfo.IsOpen = true;
                OnUIOpen(uiInfo);

            }
            return panelInstance;
        }
        else
        {

            Debug.LogError($"开启面板失败,{uiName}面板不存在");
            return null;
        }
    }

    /// <summary>
    /// 关闭指定UI面板
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public void Close<T>() where T : UIBasePanel
    {
        string uiName = typeof(T).Name;
        Close(uiName);
    }
    public void Close(string uiName)
    {
        if (UIInfos.ContainsKey(uiName))
        {
            UIInfo uiInfo = UIInfos[uiName];
            if (uiInfo.IsOpen)
            {
                uiInfo.PanelInstance.OnClose();
                uiInfo.IsOpen = false;
                OnUIClose(uiInfo);
            }
            else
            {
                Debug.LogWarning($"关闭面板失败,{uiName}面板未开启");
            }
        }
        else
        {
            Debug.LogError($"关闭面板失败,{uiName}面板不存在");
        }
    }
    /// <summary>
    /// 关闭最近打开的面板
    /// </summary>
    public bool CloseRecent()
    {
        int index = OpenedUIList.Count - 1;
        while (index >= 0 && OpenedUIList[index].ControlMode == UIControlMode.System)
        {
            index--;
        }
        if (index >= 0)
        {
            Close(OpenedUIList[index].UIName);
            return true;
        }
        return false;
    }
    public void CloseAll()
    {
        int count = OpenedUIList.Count;
        
        for (int i = count - 1; i >=0; i--)
        {
            Close(OpenedUIList[i].UIName);
        }
    }
    /// <summary>
    /// 获取指定面板的实例
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public UIBasePanel GetPanel<T>() where T : UIBasePanel
    {
        string uiName = typeof(T).Name;
        return GetPanel(uiName);
    }
    public UIBasePanel GetPanel(string uiName)
    {
        if (UIInfos.ContainsKey(uiName))
        {
            return UIInfos[uiName].PanelInstance;
        }
        else
        {
            Debug.LogError($"获取Panel失败:{uiName}不存在");
            return null;
        }
    }
    private void OnUIOpen(UIInfo uiInfo)
    {
        OpenedUIList.Add(uiInfo);
        if (uiInfo.OverlayMode == UIOverlayMode.NewPanel)
        {
            //如果当前面板不是叠加的,先调用上一个非叠加面板的关闭
            int lastPanelIndex = OpenedUIList.Count - 2;
            while (lastPanelIndex >= 0 && OpenedUIList[lastPanelIndex].OverlayMode != UIOverlayMode.NewPanel)
            {
                --lastPanelIndex;
            }
            if (lastPanelIndex >= 0)
            {
                OpenedUIList[lastPanelIndex].PanelInstance.HideUI();
            }
        }
    }
    private void OnUIClose(UIInfo uiInfo)
    {

        if (uiInfo.OverlayMode == UIOverlayMode.NewPanel)
        {
            int index = OpenedUIList.IndexOf(uiInfo);
            if (index == OpenedUIList.Count - 1)
            {
                int lastPanelIndex = OpenedUIList.Count - 2;
                while (lastPanelIndex >= 0 && OpenedUIList[lastPanelIndex].OverlayMode != UIOverlayMode.NewPanel)
                {
                    --lastPanelIndex;
                }
                if (lastPanelIndex >= 0)
                {
                    OpenedUIList[lastPanelIndex].PanelInstance.ShowUI();
                }
            }

        }
        OpenedUIList.Remove(uiInfo);
        
    }


}
public class UIInfo
{
    public string UIName;
    public bool IsCache;
    public bool IsDynamic;//是否是动态UI,例如屏幕外导航
    public UIOverlayMode OverlayMode;
    public UIControlMode ControlMode;
    public UILayer Layer;

    public UIBasePanel PanelInstance;
    public bool IsOpen = false;
}
public enum UILayer
{
    Back,
    Mid,
    Front
}
public enum UIOverlayMode
{
    NewPanel,//独立的UI界面,同时只能显示一个,例如活动页面,点击活动后跳转的页面
    Additive//叠加模式,例如弹窗
}
public enum UIControlMode
{
    System,//完全由代码控制UI的开关,例如物品不足提示UI
    Player//玩家可以主动开启或关闭,点击按钮或者使用快捷键开关
}
public class PathUtil
{
   
    public static string GetUIPath(string uiName)
    {
        return $"UI/{uiName}";
    }
    public static string GetUIDataPath()
    {
        return "Data/UIInfos";
    }

}

关键字段以及方法

使用Resources动态加载UI资源并添加对应的UI脚本,使用PathUtil存储对应的资源路径

private Dictionary<string, UIInfo> UIInfos;存放每一个UI的配置信息

private List<UIInfo> OpenedUIList;存放每一个打开的UI

public UIBasePanel Open(string uiName);打开指定名称的UI,调用对应UI的OnOpen方法,如果UI没有被缓存,先实例化出来,调用OnInit函数后再调用OnOpen方法

private void OnUIOpen(UIInfo uiInfo);当打开UI时调用,如果打开的UI的类型是NewPanel,那么调用上一个打开的NewPanel类型的UI的HideUI方法

public void Close(string uiName);关闭指定名称的UI

private void OnUIClose(UIInfo uiInfo);//当关闭UI时调用,如果关闭的UI的类型是NewPanel,那么调用上一个NewPanel类型的ShowUI方法

public bool CloseRecent();//关闭最近打开的可以由玩家控制的面板,用于使用快捷键关闭UI

预制体规范

UIRoot和Back,Mid,Front为空物体,DynamicUIRoot和StaticUIRoot为Canvas 

 每一个UI预制体下面都有一个Root节点,所有的UI元素都放到Root节点下便于管理

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐