更新
在之前做项目的学习到要让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框架,主要目的在于:
- 不需要在Scene面板提前创建好UI,界面看起来更整洁
- 对象池保证UI重复利用,减少反复生成销毁的内存开销
- 栈管理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();
}
}
}
举个![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/fcafbc957441c70f4173ee64023bacb6.png)
实现一个负责开启两个面板的主面板
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还是非常重要的。