一、背景
在一个Unity项目中或多或少需要一些UI,如设置页面,登录页面等,当页面过多时,使用一个通用的UI框架来进行针对性开发会大大减少造轮子的过程。
以下即为参考之前做过的一些项目整理出来的一个易于实现且扩展性比较强的UI界面管理的简易框架的实现思路。
二、思路概述
将所有的UI界面统一继承一个基类,在基类中实现UI的显示隐藏等UI通用功能,然后定义一个管理类将所有的界面信息放入字典中进行统一管理。重写系统的监听方法,实现对按钮的等UI的监听,并开放一个接口来给需要监听的UI来调用添加监听与相应委托事件。
三、具体实现代码及其思路梳理
1.UI路径信息
将所有的UI提前制作好预制体保存在相应路径中,建立一个UIPathData类来封装UI的路径和信息。在类中用一个字典来保存路径和名称,另设几个基本变量来保存当前UI的状态。
publicclassUIPathData{//路径保存和名称保存publicstaticDictionary<string, UIPathData> dicName =newDictionary<string, UIPathData>();//资源名称publicstring dName;//是否入栈publicbool IsPush =true;//是否关闭所有栈中的界面,用于不需要返回弹出栈中的界面,主要是一,二级界面用publicbool IsCloseAllUI =false;//默认UI缩放应该根据分辨率缩放publicfloat UIRootScale =1;}
在类的构造方法中设置界面加载所需的参数如界面资源名称、所在层级、父节点锚点类型等。
publicUIPathData(string uiName){
dName = uiName;if(!dicName.ContainsKey(dName)){
dicName.Add(dName,this);//将所有实例化的UI都添加进字典方便管理}}
然后在类中实现一些静态方法方便对字典的查询
publicstaticUIPathDataGetByName(string name){UIPathData ret =null;if(!dicName.TryGetValue(name,out ret)){returnnull;}return ret;}publicstaticvoidClearData(){
dicName.Clear();}
经过以上步骤我们便有了一个保存有UI的路径等信息的字典,并且通过实例化即可将UI加入字典来进行管理。所以在类中创建一个新的类,在类中将所有的UI进行实例化便可直接获得保存有所有UI的字典。当有新的UI需要加入项目时也只需要在此进行实例化即可。
publicclassUIPathInfo{publicstaticUIPathData UICanvas1 =newUIPathData("UICanvas1");publicstaticUIPathData UICanvas2 =newUIPathData("UICanvas2");}
2.UI的基类
对于每个UI预制体的逻辑,我们先创建一个UI基类BaseUI。在该类中用一个字典来存放当前UI界面用到的所有物体。
publicclassBaseUI:MonoBehaviour{//描述:保存当前UI界面用到的所有物体publicDictionary<string, GameObject> UI =newDictionary<string, GameObject>();//保存当前资源路径信息publicUIPathData CurPath {get;set;}}
每个UI都需要显示、隐藏、销毁及初始化的功能,于是把它们添加进基类并定义为虚方法
//初始化方法(可重写)publicvirtualvoidInit(){}//隐藏publicvoidDisActiveUI(){ gameObject.SetActive(false);}//显示publicvoidActiveUI(){ gameObject.SetActive(true);}//销毁UIpublicvoidDestroyUI(){ GameObject.Destroy(gameObject);}
添加获取所有子物体的初始化方法UIinit(),将它和Init方法在Awake()方法中调用,这样当UI资源被实例化后都能获取到所有的子物体并存入字典,接着将执行每个UI界面各自的初始化方法。
voidAwake(){UIInit();Init();}//初始化UI, 继承该基类时, 此方法必须在Awake中调用privatevoidUIInit(){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,所以在基类中将系统方法根据需要定义成虚方法,方便子类重写
publicvirtualvoidStart(){}publicvirtualvoidUpdate(){}publicvirtualvoidOnEnable(){}publicvirtualvoidOnDisable(){}publicvirtualvoidOnDestroy(){}publicvirtualvoidLateUpdate(){}
3.界面的基类
再写一个界面基类BasePanel来继承BaseUI,所有界面的UI逻辑脚本都将继承该基类。在这里重载基类的显示隐藏与销毁方法,在界面初始化时获取或添加CanvasGroup组件,用来控制界面显示与隐藏。
publicclassBasePanel:BaseUI{bool isActived;//用来记录显示状态publicbool IsActive
{get=> isActived;}CanvasGroup curRootCanvasGp;//预留处理界面包含的方法publicoverridevoidInit(){base.Init();
curRootCanvasGp = gameObject.GetComponent<CanvasGroup>();if(curRootCanvasGp==null){
curRootCanvasGp= gameObject.AddComponent<CanvasGroup>();}}}
在该类中实现显示、隐藏与销毁的方法
//显示界面publicvirtualvoidOnShow(){if(curRootCanvasGp !=null){
curRootCanvasGp.alpha =1;
curRootCanvasGp.blocksRaycasts =true;
curRootCanvasGp.interactable =true;
isActived =true;}}//隐藏不销毁publicvirtualvoidOnHide(){if(curRootCanvasGp !=null){
curRootCanvasGp.alpha =0;
curRootCanvasGp.blocksRaycasts =false;
curRootCanvasGp.interactable =false;
isActived =false;}}//关闭界面publicvirtualvoidOnClose(){DestroyUI();}
4.UI的加载管理
用单例模式定义一个UI加载管理类,在类中定义两个字典用于保存所有UI及其对应预制体资源,用以对所有的UI界面进行管理
publicclassUILoadManager:MonoBehaviour{privatestaticUILoadManager instance;publicstaticUILoadManager Instance
{get{return instance;}}//保存所有UIprivateDictionary<string, BasePanel> mallDicBasePanel =newDictionary<string, BasePanel>();privateDictionary<string, GameObject> mallDicGameObject =newDictionary<string, GameObject>();privateTransform UIParentObj;privatevoidAwake(){
instance =this;
UIParentObj =this.transform;}voidOnDestroy(){
instance =null;}}
在类中实现界面的打开,定义一个委托,当界面打开后需要执行回调函数时将作为参数传入加载UI的方法。
在加载UI时先去查找字典中是否存在该UI的记录,如果能查得到但资源丢失则直接清除记录,资源存在则直接打开。
当不存在资源时则调用资源管理层的方法来加载UI资源(这里示例调用了系统接口加载Resource文件夹里的预制体)。
publicdelegatevoidOnOpenUIDelegate(bool bSuccess,object param);publicstaticvoidShowUI(UIPathData pathdata,OnOpenUIDelegate Callback =null,object param =null){
Instance.LoadUI(pathdata, Callback, param);}voidLoadUI(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)asGameObject;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入栈voidWndUIPush(BasePanel uiPanel){if(Instance !=null){
uiPanel.ActiveUI();
uiPanel.OnShow();}}
关闭界面的方法:
//处理自身界面关闭,关闭后对应类型的栈弹出显示publicvoidOnCloseUI(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);}}voidDestroyUI(GameObject obj){Destroy(obj);}IEnumeratorDispose(GameObject obj){yieldreturnnewWaitForSeconds(1);
Resources.UnloadUnusedAssets();}
5.界面的打开与关闭
当需要关闭某个界面时只需要在界面基类的关闭界面方法添加对该静态方法的调用
UILoadManager.Instance.OnCloseUI(this);
对于管理类中打开界面方法的调用可以新建一个类来作为所有界面打开的入口
publicclassLinkManager{//无参数时:publicstaticvoidOnOpenCanvas1(){
UILoadManager.ShowUI(UIPathInfo.MainUICanvas);}//有参数时publicstaticvoidOnOpenCanvas2(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里相应的方法
publicclassMUIEventListener:EventTrigger{publicdelegatevoidVoidDelegate(GameObject go);publicVoidDelegate onClick;publicVoidDelegate onDown;publicoverridevoidOnPointerClick(PointerEventData eventData){if(onClick !=null)onClick(gameObject);}publicoverridevoidOnPointerDown(PointerEventData eventData){if(onDown !=null)onDown(gameObject);}}
然后在该类中定义一个方法来添加监听即可
publicstaticMUIEventListenerGet(GameObject go){MUIEventListener listener = go.GetComponent<MUIEventListener>();if(listener ==null)
listener = go.AddComponent<MUIEventListener>();return listener;}
当需要的时候使用以下形式给UI组件添加委托
MUIEventListener.Get(Button).onClick = Func;voidFunc(GameObject go){
Debug.Log("Button被点击了");}
四、说明
1.该框架需要将需要加载的UI界面提前制作成预制体的形式进行存储,然后每个界面的逻辑可在各自的UI上挂载各自的逻辑脚本
2.该模板只是一个初步整理的简易框架,可能描述会有不清晰,框架结构不完善考虑不周到等问题,后续会不断完善优化。
版权归原作者 Tandre_Z 所有, 如有侵权,请联系我们删除。