Unity基础UI框架

22 篇文章 0 订阅

更新

在之前做项目的学习到要让UI生成在我们制作成预制体之前的位置(一般情况下都是这样)
需要把对象池里面的生成函数变成这样

  public GameObject GetObject(string name,Transform parentTransfrom,bool InWorldSpace)
    {
        GameObject obj = null;
        if (poolDic.ContainsKey(name) && poolDic[name].Count > 0)
        {
            Debug.Log("Find item" + name);
            obj = poolDic[name][0];
            //obj.transform.position = location;
            poolDic[name].RemoveAt(0);
        }
        else
        {
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name), parentTransfrom, InWorldSpace);
            //Debug.Log(name);
        }
        obj.SetActive(true);
        return obj;
    }

生成函数改成这样

 GameObject curPanel = PoolManager.GetInstance().GetObject(GetPanelString(panelType), CanvasTransform.position, false);

其他不需要做任何修改


目的

这只是针对大家做一些小游戏的时候可能用到的UI框架,主要目的在于:

  1. 不需要在Scene面板提前创建好UI,界面看起来更整洁
  2. 对象池保证UI重复利用,减少反复生成销毁的内存开销
  3. 栈管理UI,将UI分为两类:栈顶UI、其他UI;可分别对其处理。比如实现栈顶UI可交互,其他UI不交互,这里UI指代的是面板。

设计

UI的获取和放回使用对象池实现,

实现

对象池

用于得到不同面板的预制体,遇事不决对象池

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 对象池管理器,可以调用其中的函数获取对象
/// list存储游戏物体,Dictionary制作抽屉
/// </summary>
public class PoolManager : BaseManager<PoolManager> {
    public Dictionary<string, List<GameObject>> poolDic = new Dictionary<string, List<GameObject>>();
    /// <summary>
    /// 从池子里面取得物品
    /// </summary>
    /// <param name="name"></param>
    /// <returns></returns>
    public GameObject GetObject(string name, Vector3 location) 
    {
        GameObject obj = null;
        if(poolDic.ContainsKey(name) && poolDic[name].Count > 0)
        {
            //Debug.Log("Find item" + name);
            obj = poolDic[name][0];
            obj.transform.position = location;
            poolDic[name].RemoveAt(0);
        }
        else
        {
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name), location, Quaternion.identity);
            //Debug.Log(name);
        }
        obj.SetActive(true);
        return obj;
    }
    public void PushObj(string name, GameObject obj)
    {
        obj.SetActive(false);
        //已经拥有抽屉
        if(poolDic.ContainsKey(name))
        {
            poolDic[name].Add(obj);
        }
        //里面没有抽屉
        else
        {
            poolDic.Add(name, new List<GameObject>() { obj });
        }
    }
    /// <summary>
    /// 场景切换时调用
    /// </summary>
    public void Clear()
    {
        poolDic.Clear();
    }
}

单例基类

继承的类就能开单例

public class BaseManager<T> where T : new()
{
    private static T instance;
    public static T GetInstance()
    {
        if (instance == null)
        {
            instance = new T();
            return instance;
        }
        else
        {
            return instance;
        }
    }
}

BasePanel

BasePanel,作为不同类型Panel的抽象基类,提供三个方法接口,继承Mono,其子类可以直接挂载在物体上。作用如注释。

using System.Collections;
using System.Collections.Generic;

public abstract class BasePanel:MonoBehaviour
{
    /// <summary>
    /// 开启面板的时候调用
    /// </summary>
    public abstract void OnEnter();

    /// <summary>
    /// 当前面板在栈中而不是在栈顶的时候调用
    /// </summary>
    public abstract void OnPause();

    /// <summary>
    /// 当前面板再次处于栈顶的时候调用
    /// </summary>
    public abstract void OnResume();

    /// <summary>
    /// 关闭当前面板的时候调用
    /// </summary>
    public abstract void OnExit();
}

UIManager

管理所有的UI,提供生成UI和关闭UI的接口,制定调用BasePanel的三个函数的逻辑。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 管理所有UI,由于项目并不是很大,所以现阶段没有使用json存储所有的UI类型,UI的存取通过对象池来获得
/// 在场景切换的时候需要把数据结构清空
/// </summary>
public class UIManager : BaseManager<UIManager>
{
    /// <summary>
    /// 管理当前场景的UI的栈
    /// </summary>
    private Stack<BasePanel> panelStack;
    /// <summary>
    /// 管理状态(面板)的字典
    /// </summary>
    private Dictionary<PanelType, BasePanel> panelDict;
    private Transform canvasTransform;
    /// <summary>
    /// 返回面板类型对应字符串,便于对象池生成 
    /// </summary>
    /// <param name="newPanel"></param>
    private string GetPanelString(PanelType type)
    {
        switch (type)
        {
            case PanelType.StartPanel:
                return "StartPanel";
            case PanelType.TestPanel:
                return "TestPanel";
            case PanelType.TestPanel2:
                return "TestPanel2";
            default:
                Debug.Log($"不存在{type.ToString()}面板");
                break;
        }
        return "\0";
    }
    private Transform CanvasTransform {
        get {
            if(canvasTransform == null)
            {
                canvasTransform = GameObject.Find("Canvas").transform;
            }
            return canvasTransform;
        }
    }

    public GameObject currentPanel;
    public UIManager()
    {
        panelStack = new Stack<BasePanel>();
    }
    private BasePanel GetUI(PanelType panelType)
    {
       if(panelDict == null)
        {
            panelDict = new Dictionary<PanelType, BasePanel>();
        }
        BasePanel panel;
        if(!panelDict.TryGetValue(panelType, out panel))
        {
            GameObject curPanel = PoolManager.GetInstance().GetObject(GetPanelString(panelType), CanvasTransform.position);
            curPanel.transform.SetParent(CanvasTransform);
            panel = curPanel.GetComponent<BasePanel>();
            panelDict.Add(panelType, panel);
        }
        else
        {
            panel = panelDict[panelType];
            GameObject curPanel = PoolManager.GetInstance().GetObject(GetPanelString(panelType), CanvasTransform.position);
        }
        return panel;
    }
    /// <summary>
    /// 推入一个面板
    /// </summary>
    /// <param name="newPanel"></param>
    public void PushPanel(PanelType panelType)
    {
        if(panelStack == null)
        {
            panelStack = new Stack<BasePanel>();
        }
        if(panelStack.Count > 0)
        {
            BasePanel topPanel = panelStack.Peek();
            topPanel.OnPause();
        }
        BasePanel panel = GetUI(panelType);
        panelStack.Push(panel);
        panel.OnEnter();
    }
    /// <summary>
    /// 弹出面板
    /// </summary>
    public void PopPanel()
    {
        if(panelStack == null)
        {
            panelStack = new Stack<BasePanel>();
        }
        if(panelStack.Count<=0)
        {
            return;
        }
        //退出栈顶面板
        BasePanel topPanel = panelStack.Pop();
        topPanel.OnExit();

        //恢复上一个面板
        if (panelStack.Count > 0)
        {
            BasePanel panel = panelStack.Peek();
            panel.OnResume();
        }
    }
}

举个在这里插入图片描述

实现一个负责开启两个面板的主面板
testButton1可以用find也可以直接拖拽赋值

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class StartPanel : BasePanel
{
    public Button testButton1;
    public Button testButton2;
    private CanvasGroup m_CanvasGroup;
    private void Start()
    {
        testButton1.onClick.AddListener(test1ButtonEvent);
        testButton2.onClick.AddListener(test2ButtonEvent);
        m_CanvasGroup = gameObject.GetComponent<CanvasGroup>();
    }
    public override void OnEnter()
    {
        Debug.Log("进入开始面板");
    }
    private void test1ButtonEvent()
    {
        UIManager.GetInstance().PushPanel(PanelType.TestPanel);
    }
    private void test2ButtonEvent()
    {
        UIManager.GetInstance().PushPanel(PanelType.TestPanel2);
    }
    public override void OnExit()
    {}

    public override void OnPause()
    {
        Debug.Log("开始界面被覆盖");
        m_CanvasGroup.blocksRaycasts = false;
    }

    public override void OnResume()
    {
        m_CanvasGroup.blocksRaycasts = true;
    }
}

GameManager

这里只负责一开始生成开始面板,挂载到主相机上面

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        UIManager.GetInstance().PushPanel(PanelType.StartPanel);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

运行结果如下
在这里插入图片描述

TestPanel1/ TestPanel2

测试面板,只能开启其中一个,一个开启后,StartPanel的按钮将不会再生效,直到关闭

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TestPanel : BasePanel
{
    public Button exitButton;
    private CanvasGroup m_CanvasGroup;
    private void Start()
    {
        exitButton.onClick.AddListener(exitButtonAction);
    }

    public override void OnEnter()
    {
        Debug.Log("打开测试界面1");
    }

    public override void OnExit()
    {
        Debug.Log("测试界面1退出");
        PoolManager.GetInstance().PushObj("TestPanel", this.gameObject);
    }

    public override void OnPause()
    {
        Debug.Log("测试界面1被覆盖");
        m_CanvasGroup.blocksRaycasts = false;
    }

    public override void OnResume()
    {
        Debug.Log("测试界面1恢复");
        m_CanvasGroup.blocksRaycasts = true;
    }
    private void exitButtonAction()
    {
        UIManager.GetInstance().PopPanel();
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TestPanel2 : BasePanel
{
    public Button exitButton;
    private CanvasGroup m_CanvasGroup;
    private void Start()
    {
        exitButton.onClick.AddListener(exitButtonAction);
    }
    public override void OnEnter()
    {
        Debug.Log("打开测试界面2");
    }

    public override void OnExit()
    {
        Debug.Log("测试界面2退出");
        PoolManager.GetInstance().PushObj("TestPanel2", this.gameObject);
    }

    public override void OnPause()
    {
        Debug.Log("测试界面2被覆盖");
        m_CanvasGroup.blocksRaycasts = false;
    }

    public override void OnResume()
    {
        Debug.Log("测试界面2恢复");
        m_CanvasGroup.blocksRaycasts = true;
    }
    private void exitButtonAction()
    {
        UIManager.GetInstance().PopPanel();
    }
}

分别挂载到两个面板上
在这里插入图片描述
在这里插入图片描述
接下来测试
1. 按下测试按钮一、testPanel1出现
在这里插入图片描述

2. 尝试按下测试按钮二、没有反应
3. 关闭测试面板一后再次按下测试按钮二,出现测试面板二,并且一失活进入对象池
在这里插入图片描述

4. 同理尝试按下测试按钮一、没有反应
ヾ(✿゚▽゚)ノ说明成功了
ps:这里生成的UI并没有按照预制体之前的位置放在左右上角,根据更新所示的那样就行了

总结

很多情况下,当一个UI出现时我们都需要把其他UI的交互关闭,否则经常会造成各种bug,所以管理UI还是非常重要的。

  • 7
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值