一、背景
在一个Unity项目中或多或少需要一些UI,如设置页面,登录页面等,当页面过多时,使用一个通用的UI框架来进行针对性开发会大大减少造轮子的过程。
以下即为参考之前做过的一些项目整理出来的一个易于实现且扩展性比较强的UI界面管理的简易框架的实现思路。
二、思路概述
将所有的UI界面统一继承一个基类,在基类中实现UI的显示隐藏等UI通用功能,然后定义一个管理类将所有的界面信息放入字典中进行统一管理。重写系统的监听方法,实现对按钮的等UI的监听,并开放一个接口来给需要监听的UI来调用添加监听与相应委托事件。
三、具体实现代码及其思路梳理
1.UI路径信息
将所有的UI提前制作好预制体保存在相应路径中,建立一个UIPathData类来封装UI的路径和信息。在类中用一个字典来保存路径和名称,另设几个基本变量来保存当前UI的状态。
public class UIPathData
{
//路径保存和名称保存
public static Dictionary<string, UIPathData> dicName = new Dictionary<string, UIPathData>();
//资源名称
public string dName;
//是否入栈
public bool IsPush = true;
//是否关闭所有栈中的界面,用于不需要返回弹出栈中的界面,主要是一,二级界面用
public bool IsCloseAllUI = false;
//默认UI缩放应该根据分辨率缩放
public float UIRootScale = 1;
}
在类的构造方法中设置界面加载所需的参数如界面资源名称、所在层级、父节点锚点类型等。
public UIPathData(string uiName)
{
dName = uiName;
if (!dicName.ContainsKey(dName))
{
dicName.Add(dName, this);//将所有实例化的UI都添加进字典方便管理
}
}
然后在类中实现一些静态方法方便对字典的查询
public static UIPathData GetByName(string name)
{
UIPathData ret = null;
if (!dicName.TryGetValue(name, out ret))
{
return null;
}
return ret;
}
public static void ClearData()
{
dicName.Clear();
}
经过以上步骤我们便有了一个保存有UI的路径等信息的字典,并且通过实例化即可将UI加入字典来进行管理。所以在类中创建一个新的类,在类中将所有的UI进行实例化便可直接获得保存有所有UI的字典。当有新的UI需要加入项目时也只需要在此进行实例化即可。
public class UIPathInfo
{
public static UIPathData UICanvas1 = new UIPathData("UICanvas1");
public static UIPathData UICanvas2 = new UIPathData("UICanvas2");
}
2.UI的基类
对于每个UI预制体的逻辑,我们先创建一个UI基类BaseUI。在该类中用一个字典来存放当前UI界面用到的所有物体。
public class BaseUI : MonoBehaviour
{
//描述:保存当前UI界面用到的所有物体
public Dictionary<string, GameObject> UI = new Dictionary<string, GameObject>();
//保存当前资源路径信息
public UIPathData CurPath { get; set; }
}
每个UI都需要显示、隐藏、销毁及初始化的功能,于是把它们添加进基类并定义为虚方法
//初始化方法(可重写)
public virtual void Init(){}
//隐藏
public void DisActiveUI(){ gameObject.SetActive(false); }
//显示
public void ActiveUI(){ gameObject.SetActive(true); }
//销毁UI
public void DestroyUI(){ GameObject.Destroy(gameObject); }
添加获取所有子物体的初始化方法UIinit(),将它和Init方法在Awake()方法中调用,这样当UI资源被实例化后都能获取到所有的子物体并存入字典,接着将执行每个UI界面各自的初始化方法。
void Awake()
{
UIInit();
Init();
}
//初始化UI, 继承该基类时, 此方法必须在Awake中调用
private void UIInit()
{
Transform[] childsAll = GetComponentsInChildren<Transform>(true);
UI.Clear();
for (int i = 0; i < childsAll.Length; i++)
{
if (childsAll[i].CompareTag("UI"))
{
if (UI.ContainsKey(childsAll[i].name))
{
continue;
}
UI.Add(childsAll[i].name, childsAll[i].gameObject);
}
}
}
因为只有基类直接继承了MonoBehaver,所以在基类中将系统方法根据需要定义成虚方法,方便子类重写
public virtual void Start(){}
public virtual void Update(){}
public virtual void OnEnable(){}
public virtual void OnDisable(){}
public virtual void OnDestroy(){}
public virtual void LateUpdate(){}
3.界面的基类
再写一个界面基类BasePanel来继承BaseUI,所有界面的UI逻辑脚本都将继承该基类。在这里重载基类的显示隐藏与销毁方法,在界面初始化时获取或添加CanvasGroup组件,用来控制界面显示与隐藏。
public class BasePanel : BaseUI
{
bool isActived;//用来记录显示状态
public bool IsActive
{
get => isActived;
}
CanvasGroup curRootCanvasGp;
//预留处理界面包含的方法
public override void Init()
{
base.Init();
curRootCanvasGp = gameObject.GetComponent<CanvasGroup>();
if (curRootCanvasGp==null)
{
curRootCanvasGp= gameObject.AddComponent<CanvasGroup>();
}
}
}
在该类中实现显示、隐藏与销毁的方法
//显示界面
public virtual void OnShow()
{
if (curRootCanvasGp != null)
{
curRootCanvasGp.alpha = 1;
curRootCanvasGp.blocksRaycasts = true;
curRootCanvasGp.interactable = true;
isActived = true;
}
}
//隐藏不销毁
public virtual void OnHide()
{
if (curRootCanvasGp != null)
{
curRootCanvasGp.alpha = 0;
curRootCanvasGp.blocksRaycasts = false;
curRootCanvasGp.interactable = false;
isActived = false;
}
}
//关闭界面
public virtual void OnClose()
{
DestroyUI();
}
4.UI的加载管理
用单例模式定义一个UI加载管理类,在类中定义两个字典用于保存所有UI及其对应预制体资源,用以对所有的UI界面进行管理
public class UILoadManager : MonoBehaviour
{
private static UILoadManager instance;
public static UILoadManager Instance
{
get { return instance; }
}
//保存所有UI
private Dictionary<string, BasePanel> mallDicBasePanel = new Dictionary<string, BasePanel>();
private Dictionary<string, GameObject> mallDicGameObject = new Dictionary<string, GameObject>();
private Transform UIParentObj;
private void Awake()
{
instance = this;
UIParentObj = this.transform;
}
void OnDestroy()
{
instance = null;
}
}
在类中实现界面的打开,定义一个委托,当界面打开后需要执行回调函数时将作为参数传入加载UI的方法。
在加载UI时先去查找字典中是否存在该UI的记录,如果能查得到但资源丢失则直接清除记录,资源存在则直接打开。
当不存在资源时则调用资源管理层的方法来加载UI资源(这里示例调用了系统接口加载Resource文件夹里的预制体)。
public delegate void OnOpenUIDelegate(bool bSuccess, object param);
public static void ShowUI(UIPathData pathdata, OnOpenUIDelegate Callback = null, object param = null)
{
Instance.LoadUI(pathdata, Callback, param);
}
void LoadUI(UIPathData uiData, OnOpenUIDelegate Callback = null, object param = null, UnityEngine.Object curAssetObj = null)
{
if (Instance == null)
{
return;
}
GameObject uiobj = null;
BasePanel uipanel = null;
//加载或获取当前界面
if (mallDicGameObject.TryGetValue(uiData.dName, out uiobj))//字典中已经记录了该UI物体
{
if (uiobj == null)//资源不存在了
{
mallDicGameObject.Remove(uiData.dName);//移除该记录
if (Instance.mallDicBasePanel.ContainsKey(uiData.dName))
{
Instance.mallDicBasePanel.Remove(uiData.dName);
}
}
if (Instance.mallDicBasePanel.TryGetValue(uiData.dName, out uipanel))
{
if (uipanel == null)
{
Instance.mallDicBasePanel.Remove(uiData.dName);
}
}
}
if (uiobj == null)
{
//资源参数赋值
UnityEngine.Object tempw = curAssetObj;
if (tempw == null)
{
//Res加载
//tempw = ResLoadManager.LoadResAsset(uiData.dName, ResLoadManager.AssetsEnum.UIPrefab);
tempw=Resource.Load(uiData.dName,ResLoadManager)
}
if (tempw != null)
{
uiobj = Instantiate(tempw) as GameObject;
BasePanel temppanel = uiobj.GetComponent<BasePanel>();
temppanel.CurPath = uiData;
uiobj.transform.parent = UIParentObj;
mallDicGameObject.Add(uiData.dName, uiobj);
if (!Instance.mallDicBasePanel.ContainsKey(uiData.dName))
{
Instance.mallDicBasePanel.Add(uiData.dName, temppanel);
uipanel = temppanel;
}
else
{
uipanel = Instance.mallDicBasePanel[uiData.dName];
}
}
}
Instance.WndUIPush(uipanel);
if (Callback != null)
{
Callback(uiobj != null, param);
}
}
//UI入栈
void WndUIPush(BasePanel uiPanel)
{
if (Instance != null)
{
uiPanel.ActiveUI();
uiPanel.OnShow();
}
}
关闭界面的方法:
//处理自身界面关闭,关闭后对应类型的栈弹出显示
public void OnCloseUI(BasePanel uiPanel)
{
//根据当前界面类型 处理关闭界面
if (Instance == null)
return;
//移除对应集合中的数据
Debug.Log(uiPanel.CurPath.dName);
if (mallDicBasePanel.ContainsKey(uiPanel.CurPath.dName))
{
mallDicBasePanel.Remove(uiPanel.CurPath.dName);
}
List<BasePanel> tempStack = null;
if (uiPanel.CurPath.IsCloseAllUI)
{
//关闭所有
for (int i = 0; i < tempStack.Count; i++)
{
BasePanel temp = tempStack[i];
if (mallDicBasePanel.ContainsKey(temp.CurPath.dName))
{
mallDicBasePanel.Remove(temp.CurPath.dName);
}
DestroyUI(temp.gameObject);
}
tempStack.Clear();
}
else
{
if (mallDicBasePanel.ContainsKey(uiPanel.CurPath.dName))
{
mallDicBasePanel.Remove(uiPanel.CurPath.dName);
}
DestroyUI(uiPanel.gameObject);
}
}
void DestroyUI(GameObject obj)
{
Destroy(obj);
}
IEnumerator Dispose(GameObject obj)
{
yield return new WaitForSeconds(1);
Resources.UnloadUnusedAssets();
}
5.界面的打开与关闭
当需要关闭某个界面时只需要在界面基类的关闭界面方法添加对该静态方法的调用
UILoadManager.Instance.OnCloseUI(this);
对于管理类中打开界面方法的调用可以新建一个类来作为所有界面打开的入口
public class LinkManager
{
//无参数时:
public static void OnOpenCanvas1()
{
UILoadManager.ShowUI(UIPathInfo.MainUICanvas);
}
//有参数时
public static void OnOpenCanvas2(object param1)
{
//UILoadManager.ShowUI(UIPathInfo.KnowledgeCatalogUICanvas);
UILoadManager.ShowUI(UIPathInfo.KnowledgeCatalogUICanvas, (bool b, object param) =>
{
if (b)
{
if (Canvas1.Instance)
{
Debug.Log("执行了回调方法");
}
}
});
}
}
6.UI事件的监听
对于UI事件的监听,先定义一个事件触发器,该类继承EventTrigger,在类中定义所需要监听的事件的委托,然后重写EventTrigger里相应的方法
public class MUIEventListener : EventTrigger
{
public delegate void VoidDelegate(GameObject go);
public VoidDelegate onClick;
public VoidDelegate onDown;
public override void OnPointerClick(PointerEventData eventData)
{
if (onClick != null) onClick(gameObject);
}
public override void OnPointerDown(PointerEventData eventData)
{
if (onDown != null) onDown(gameObject);
}
}
然后在该类中定义一个方法来添加监听即可
public static MUIEventListener Get(GameObject go)
{
MUIEventListener listener = go.GetComponent<MUIEventListener>();
if (listener == null)
listener = go.AddComponent<MUIEventListener>();
return listener;
}
当需要的时候使用以下形式给UI组件添加委托
MUIEventListener.Get(Button).onClick = Func;
void Func(GameObject go)
{
Debug.Log("Button被点击了");
}
四、说明
1.该框架需要将需要加载的UI界面提前制作成预制体的形式进行存储,然后每个界面的逻辑可在各自的UI上挂载各自的逻辑脚本
2.该模板只是一个初步整理的简易框架,可能描述会有不清晰,框架结构不完善考虑不周到等问题,后续会不断完善优化。