0


Unity实现一个超好用的UI管理器,不需要继承Base基类

前言:在网上关于UI管理器的教程比比皆是,它们的实现方式虽有不同,但是大致上的逻辑基本上都是这样的:所有的UI面板继承自一个面板基类,通常命名为BasePanel,BasePanel继承Monobehaviour,其中一般会有两个方法:打开界面和关闭界面。接着创建具体的界面UI和继承BasePanel的子类,最后再写一个UIManager负责管理这些界面的显示和隐藏。这种方式有很大的规范性,所以——今天我的文章内容就是我最近自己写的一个UI管理器脚本,只需要一个脚本就可以,所有的UI界面都不需要继承任何基类,你甚至可以不给界面添加脚本,废话不多说直接开始吧~

首先给大家看一个雏形版的(觉得低级的可以直接看后面的进阶版),只需要创建这个代码,然后在场景中创建一个空物体,命名为UI(或者别的名字),挂载这个代码就可以了,内容很基础很基础,非常的傻瓜式代码:

using UnityEngine;

/// <summary>
/// UI管理器
/// </summary>

public class UIManager : MonoBehaviour
{
    #region 单例模式
    public static UIManager instance;
    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
    #endregion

    //打开一个界面
    public void OpenUI(string uiName)
    {
        GameObject obj = transform.Find(uiName).gameObject;
        if(obj != null)
        {
            obj.SetActive(true);
        }
    }
    //关闭一个界面
    public void CloseUI(string uiName)
    {
        GameObject obj = transform.Find(uiName).gameObject;
        if (obj != null)
        {
            obj.SetActive(false);
        }
    }

    //获取一个界面上的脚本
    public T GetUI<T>(string uiName)
    {
        T t = transform.Find(uiName).GetComponent<T>();
        if (t != null)
        {
            return t;
        }
        else
        {
            return default(T);
        }
    }
}

这里我就简要介绍一下代码内容,其实很多初学者应该都能看懂,首先为了在其他脚本中访问我们的UI管理器,上来先实现一个单例模式:

然后就是我们最主要的两个方法了,OpenUI:

在介绍这段代码之前先说一下我们的前提条件:上面说了在场景中新建一个空物体并挂载这个代码:

然后我们所有的UI界面都会放在这个空物体下面,比如我这里创建了三个界面:

前提条件说完了,再来说OpenUI方法中的代码,我们传入一个string类型的变量,这个变量其实就是UI界面的名称,我们想要显示哪个界面就传入哪个界面的名称就可以了。方法中的第一行代码

在transform下查找对应参数名称的子物体,因为这个脚本挂载到了所有界面的父物体上,所以只要名称拼写正确的话这种方法是可以查找到的。然后我们判断

这个要查找的物体不为空就显示它就可以了。其实这里也可以再加一个判断:那就是if (obj.activeInHierarchy) 判断这个物体是否已经激活了,那么我们就不必再次激活它。

然后是CloseUI方法:

还是传入一个名称变量,在transform的子级下查找这个名称的界面,如果不为空就隐藏。

接着就是一个可有可无的方法,GetUI:

在实际开发中有时候经常会遇到这样的需求:在显示一个界面的同时更新这个界面上的内容,比如打开玩家信息界面,这个界面上挂载了一个玩家信息脚本,我想在打开界面的时候调用这个脚本中的初始化方法。那么我们就用到了这个GetUI方法,这个方法返回一个泛型T,所以你在界面上挂载什么脚本都可以,还是传入一个名称变量,声明一个泛型变量 T t 查找对应的子物体身上挂载的 T 类型的组件,然后我们返回这个T就可以了。

代码介绍完了,是不是特别简单,都是一些基础的知识,接下来说使用方法:当我们想显示某一个界面时,只需要在任何地方调用:

UIManager.instance.OpenUI("菜单界面");

当我们想关闭某个界面时:

UIManager.instance.CloseUI("菜单界面");

当我们想获取界面身上的脚本时(假设我的游戏界面上有一个PlayerControl脚本,脚本中有一个init方法):

PlayerControl control = UIManager.instance.GetUI<PlayerControl>("游戏界面");
control.Init();

基础版的代码讲完了,应该都能看懂吧,这个简易的UI管理器只适用于轻量级的小游戏中,UI界面不是很多的项目。基础版的介绍完毕,接下来该介绍进阶版的了,这个进阶版没有太多的规范内容,也可以适用于UI界面特别多的项目,代码中还是三个主要方法:打开界面OpenUI(),关闭界面CloseUI(),获取界面脚本GetUI()。

先说前提条件,跟之前一样还是新建一个空物体,并挂载UIManager脚本。然后我们把我们的界面保存为预制体,这里我保存在了Resources目录下:

采用加载的方式来显示UI界面,这样比起之前将所有界面都放在UI空物体的子级下,会让场景中的物体更少。

然后我们来看代码,这里就不直接放完整的源码了,需要源码的小伙伴可以在后台私信我或者评论区留言,看到了第一时间会回复你的(无偿的哦)

首先我们需要创建一个表示UI界面名称的枚举:

//UI界面枚举
public enum UIName
{
    菜单界面,
    设置界面,
    游戏界面,
}

枚举其实是非必须的,因为我们的UI界面可能有不挂载脚本的,这样做的目的是为了方便管理,同时代码写起来有提示😄

接着我们写一个表示界面保存路径的类,其中声明一个字典,键为UIName枚举类型,表示哪一个界面;值为string类型,表示这个界面保存的路径:

//UI界面路径类
public class PanelPath
{
    public static Dictionary<UIName, string> panels = new Dictionary<UIName, string>
    {
        {UIName.菜单界面,"Panels/Menu/主界面" },
        {UIName.设置界面,"Panels/Menu/设置界面" },
        {UIName.游戏界面,"Panels/Game/游戏界面" },
    };
}

这个路径类也不是必须的,还是为了方便管理,如果你的界面预制体比较多,需要分文件夹来存放,你就需要使用这个路径类,如果你的界面预制体不是很多,只放在一个文件夹中,那么你只需要在加载时把路径写死就可以了,不需要写这个路径类了。我是分文件夹保存的,因为我的项目中界面一般分为菜单中的界面和游戏中的界面,请参考上面截图我保存的界面结构。

不要忘了这个UIManager需要写单例模式

有了枚举和路径,接下来我们来实现OpenUI方法:

  //打开一个界面
    public void OpenUI(UIName uiName)
    {
        //判断这个界面是否已经被加载了
        if (transform.Find(uiName.ToString()) != null)
        {
            //获取界面物体
            GameObject panel = transform.Find(uiName.ToString()).gameObject;
            //判断该物体是否已经被激活
            if (panel.activeInHierarchy)
            {
                Debug.LogError("此界面已经显示!");
                return;
            }
            //激活这个界面
            panel.SetActive(true);
            return;
        }
        //如果未被加载 则加载
        GameObject obj = Resources.Load<GameObject>(PanelPath.panels[uiName]);
        //安全检查 如果该界面为空
        if (obj == null)
            Debug.LogError(PanelPath.panels[uiName] + " 路径下未找到该界面!");
​
        //实例化
        GameObject ui = Instantiate(obj, transform);
        //重命名这个物体
        ui.name = uiName.ToString();
    }

代码详解:传入的参数为UIName枚举类型,因为我们加载出来的界面需要放在挂载UIManager脚本的空物体层级下,所以还是使用transform.Find()这个API来查找子物体,这一行是在判断子级下是否有这个传入参数的名称(uiName.ToString()就是枚举的值)的子物体存在:

如果这个界面存在,那也可能有两种情况:一个是这个界面已经显示了,处于激活状态。还有一个就是它虽然存在于场景中,但是隐藏了,处于未激活状态。既然确定这个界面物体不为空了,所以我们先获取这个物体,判断它是否是激活状态:

如果是激活状态的话,那我们也没必要再显示一个这个界面了,直接输出一个错误信息,然后return不向下执行:

如果不是激活状态,那我们就激活它并return不向下执行:

在上面的代码中,如果判断成立的话都会return掉,也就是说只要代码执行到这个位置就说明在UIManager的transform子级下没有不存在这个名称的界面,那么我们就需要加载这个界面:

因为我将所有界面的预制体都保存在了Resources目录下,所以这里我使用了Resources.Load这个加载资源的API,加载路径就是之前创建的路径类中的字典的值,方法传入的参数就是我们填写对应的键。然后 if (obj == null) 是一个安全检查,如果这个路径下没有这个物体,那么就打印错误信息。

最后如果不为空我们就实例化这个物体,并放在UIManager的子级下,接着以枚举的值来命名这个界面(因为实例化出来的物体名称结尾会有一个“(clone)”)

接下来是关闭界面CloseUI()方法:

这个方法传入两个参数,第一个参数是对应的UIName枚举,第二个参数是一个bool类型的变量,表示是否删除这个界面,我们给它一个默认值不删除(因为我的项目中不删除的比较多)。因为Unity使用动态加载物体是比较耗费性能的,所以如果经常要显示的界面我们就不删除它,只需要将这个界面 SetActive(false); 就可以了,而且在OpenUI()方法中也有这个未激活情况的判断。

这里举个🌰:比如玩家在游戏中需要经常打开背包来查看子级的物体,那么背包界面就是一个常用的界面,当我们想关闭这个界面时,就不要从场景中删除了,而是隐藏它。再举个🌰:假设玩家通关时我们需要显示一个胜利界面,通过一关要很久,所以这个胜利界面不经常显示,我们在关闭这个界面的时候删除它,避免占用资源是比较合适的选择。

 //关闭一个界面 参数:是否删除 默认不删除
 public void CloseUI(UIName uiName, bool isDel = false)
 {
     //查找子物体
     if (transform.Find(uiName.ToString()) == null)
     {
         Debug.LogError("此界面不存在于场景中");
         return;
     }
     GameObject panel = transform.Find(uiName.ToString()).gameObject;
     //关闭界面
     if (isDel)
     {
         Destroy(panel); //删除
     }
     else
     {
         panel.SetActive(false); //隐藏
     }
 }

首先方法的第一行还是判断场景中有没有这个界面物体,如果没有的话那也无法关闭一个不存在的物体,直接报错误信息并返回。

然后我们还是在UIManager的子级下获取这个对应名称的子物体界面:

接下来就该关闭这个界面了,判断传入的第二个参数,来决定是删除这个界面还是隐藏这个界面。

接下来是获取界面脚本GetUI()方法:

    //获取一个界面身上的脚本
    public T GetUI<T>(UIName uiName)
    {
        T t = transform.Find(uiName.ToString()).GetComponent<T>();
        if (t != null)
        {
            return t;
        }
        else
        {
            Debug.LogError("未找到脚本!");
            return default(T);
        }
    }

这个方法跟上面基础版的UIManager一样,我就不再介绍了。

最后说一下使用方法,可以在任何地方调用:

当我们想显示某一个界面时:

UIManager.instance.OpenUI(UIName.菜单界面);

当我们想关闭某个界面时:

UIManager.instance.CloseUI(UIName.菜单界面)

当我们想获取界面身上的脚本时(假设我的游戏界面上有一个MenuPanel脚本,脚本中有一个init方法):

MenuPanel menu = UIManager.instance.GetUI<MenuPanel>("游戏界面");
menu.Init();

代码介绍完了,其实这个UI管理器的规范很简单,只需要将加载出来的UI界面设置同一个父物体(推荐为挂载UIManager脚本的空物体),再以枚举值来命名,这就是我们的规范了,另外将对应的UI界面预制体的名称改为和枚举值一样的,怎么去分配上面也有截图,非常的简单。

最后说一下注意事项和其他的内容:

首先是存储路径的建议,我的建议是将UI预制体放在Resources文件目录下,因为无论我们将游戏打包到哪个平台,都可以使用Resources.Load() 这个API,小伙伴们如果想放在其他文件目录中一定要根据子级的目标平台来决定。

还有就是我们的界面搭建问题,一般来说有两种方式

第一种:所有的界面比如菜单、设置、游戏等预制体都是一个独立的Canvas,

第二种:所有的界面统一使用一个Canvas做父物体(就是加载时设置的UI空物体),每个界面都是一个Canvas下的子物体,可以是Panel,也可以是图片、文字等。

让我们来分析一下这两种方法的优缺点

首先我们需要知道一件事情:当UI上的某个内容发生变化时(举个🌰:比如你更改了一次文本,增加玩家分数)那么整个Canvas都会进行一次重新绘制,所以当你的一个Canvas下如果有大量需要经常更新的子元素时,这会导致性能降低。所以如果每个Canvas独立的话,可以只重绘特定的Canvas,更新某一Canvas的UI时不会影响其他Canvas,而不是整个界面。这样做大大节省了性能开销,特别是在大量动态更新的环境下。

那么我们把每个界面都使用独立的Canvas就是最优解了吗?当然也不是。同样还有我们需要知道的一件事情:多个Canvas会增加Draw Call的数量,每个Canvas在屏幕上更新时都需要进行渲染,如果Canvas数量较多,就会产生更多的Draw Call。

总结:所以这两种方法各有优势,我们需要根据自己项目的情况来决定如何选择,如果你的UI界面相对静态,且变化不频繁,使用单一Canvas管理多个子元素通常会更高效,能够减少绘制调用。如果你的UI界面变化频繁(如实时数据展示,动态交互等),使用独立的Canvas可能更合适,因为可以减少重绘的开销。

以上就是关于Unity的UI管理器全部的内容啦,有任何问题或建议直接在评论区留言就可以了,小伙伴们如果觉得有帮助的话就点个​关注支持一下吧~

感兴趣的小伙伴也可以关注一下我的公众号【Unity初级开发者】,除了Unity知识点文章外还有免费的游戏开发资产分享哦~

标签: unity ui 游戏引擎

本文转载自: https://blog.csdn.net/m0_60939640/article/details/143004585
版权归原作者 zhing5546 所有, 如有侵权,请联系我们删除。

“Unity实现一个超好用的UI管理器,不需要继承Base基类”的评论:

还没有评论