1. 程式人生 > >unity介面和程式碼分離解決方案

unity介面和程式碼分離解決方案

新手或者小規模的遊戲喜歡使用SetActive或者Instantiate prefab來切換介面,這樣可以,但是不靈活,也不規範,碰到大量程式碼的時候就會力不從心。


所謂介面與程式碼分離也就是將介面用程式碼封裝起來,再通過一個管理器來控制,方便其他程式的呼叫和控制


介紹一下大體設計方法,讓大家有一個巨集觀的觀念


介面一共分為兩種
    1.Panel 面板
        比如登陸面板,註冊面板,進入遊戲的主菜單面板,畫面設定面板等等,即擁有一定相關功能的介面模組
    2.Tip 提示
        比如錯誤提示,勝利提示,失敗提示,好友訊息等等,即主要功能只是反饋資料資訊的小介面


所有介面類都將繼承PanelBase基類,通過修改每個子類的Layer列舉型別欄位來設定是Panel還是Tip(PanelBase可能有歧義,但是他是所有UI的Base)


所有的UI類都繼承MonoBehaviour,指令碼統一掛到Canvas畫布上,用PanelMgr管理類通過一個字典<string,PanelBase>來控制,介面的UI也就是具體的GameObject則是指令碼內的一個GameObject欄位,具體的其他按鈕什麼的通過該欄位的Transform快取後在例項時查詢賦值
這兩種Panel和Tip介面分別屬於Canvas/Panel和Canvas/Tip兩個GameObject的子物體,用PanelMgr管理類通過一個字典<PanelLayer,Transform>來控制
生命週期可以用子類的重寫


好,下面上程式碼,讓我們看一下詳細的設計方法

public enum PanelLayer
{
    Panel,        //面板
    Tip,         //提示
}

UI基類
public class PanelBase : MonoBehaviour {


    //面板路徑
    public string skinPath;
    //面板
    public GameObject skin;
    //層級
    public PanelLayer layer;
    //面板引數
    public object[] args;


    #region 生命週期
    public virtual void Init(params object[] args)   //自定義引數
    {
        this.args = args;
    }


    //開始面板前
    public virtual void OnShowing() { }


    //顯示面板後
    public virtual void OnShowed() { }


    //幀更新
    public virtual void Update() { }


    //關閉前
    public virtual void OnClosing() { }


    //關閉後
    public virtual void OnClosed() { }
    #endregion


    #region 操作
    protected virtual void Close()
    {
        string name = this.GetType().ToString();   //反射
        PanelMgr.Instance.ClosePanel(name);
    }
    #endregion
}


關於子類如何重寫來使用,這裡先留一個懸念,最後我們在舉一個例子看看怎麼用


管理類
public class PanelMgr : Singletion<PanelMgr>   //單例
{
    //畫板
    private GameObject canvas;


    //各個面板
    public Dictionary<string, PanelBase> dict;


    //各個層級
    private Dictionary<PanelLayer, Transform> layerDict;


    //開始
    public void Awake()
    {
        InitLayer();
        dict = new Dictionary<string, PanelBase>();
    }


    //初始化層
    private void InitLayer()
    {
        //畫布
        canvas = GameObject.Find("Canvas");
        if (canvas == null)
            Debug.LogError("PanelMgr.InitLayer fail,canvas is null");


        //各個層級
        layerDict = new Dictionary<PanelLayer, Transform>();


        foreach (PanelLayer pl in Enum.GetValues(typeof(PanelLayer))) // Canvas/Panel和Canvas/Tip 找到這兩個物體,讓UI掛在下面
        {
            string name = pl.ToString();
            Transform transform = canvas.transform.Find(name);
            layerDict.Add(pl, transform);
        }
    }


    //打開面板
    public void OpenPanel<T>(string skinPath, params object[] args) where T : PanelBase   //T必須是PanelBase的子類
    {
        //已經開啟
        string name = typeof(T).ToString();
        if (dict.ContainsKey(name))
            return;


        //面板指令碼
        PanelBase panel = canvas.AddComponent<T>();  //把指令碼掛載Canvas下
        panel.Init(args);    //生命週期
        dict.Add(name, panel);
        //載入面板
        skinPath = skinPath != "" ? skinPath :  panel.skinPath;   //skinPath也就是該Prefab的路徑
        skinPath = "Prefabs/UISkin/" + skinPath;
        GameObject skin = Resources.Load<GameObject>(skinPath);
        if(skinPath==null)
            Debug.LogError("panelMgr.OpenPanel fail,skin is null,skinPath = "+skinPath);
        panel.skin = Instantiate(skin);//加載出來


        //座標
        Transform skintrans = panel.skin.transform;   //得到該介面的Layer,然後從layerDict中找到對應的Layer的Transform,賦值過去成為子物體
        PanelLayer layer = panel.layer;
        Transform parent = layerDict[layer];
        skintrans.SetParent(parent, false); //層級


        //panel的生命週期
        panel.OnShowing();   //預留的面板動畫
        //anm
        panel.OnShowed();   //載入結束時
    }


    //關閉面板
    //注意,name是該UI類的反射名,注意前面的名稱空間,巢狀類記得寫+號!
    public void ClosePanel(string name)
    {
        PanelBase panel;
        if (dict.ContainsKey(name))
        {
            panel = dict[name];
        }
        else
            return;


        panel.OnClosing();
        dict.Remove(name);
        panel.OnClosed();
        Destroy(panel.skin);
        Destroy(panel);
    }
     //這裡有必要補充一下,假如是UI.Panel名稱空間下的LobbyPanel類中的巢狀類AchieveTip,則應該呼叫ClosePanel("UI.Panel.LobbyPanel+AchieveTip");
}


總結一下,管理類內部的介面字典是用每個介面的反射類名來儲存,如果已經顯示了某個介面,則不能再顯示,並將每個介面存在介面對應的Layer對應的Transform下面,OpenPanel方法使用泛型,關閉介面則是從字典中查詢刪除,非常好理解。


下面我們看一個子類具體是怎麼使用和定義
比如一個簡單的登入面板
擁有兩個輸入框和一個確定按鈕和一個註冊按鈕,以及若干Text
namespace UI.Panel
{
    public class LoginPanel : PanelBase
    {
        private InputField inputFieldName;
        private InputField inputFieldPwd;
        private Button btnLogin;
        private Button btnReg;   //在這裡宣告介面上的一些按鈕什麼的


        #region 生命週期
        public override void Init(params object[] args)
        {
            base.Init(args);


            skinPath = "Panel/MainMenu/LoginPanel";//設定具體的Prefab路徑
            layer = PanelLayer.Panel;  //定義Layer
        }


        public override void OnShowing()//在這裡查詢賦值
        {
            base.OnShowing();


            Transform skinTrans = skin.transform;
            inputFieldName = skinTrans.Find("InputFieldName").GetComponent<InputField>();
            inputFieldPwd = skinTrans.Find("InputFieldPwd").GetComponent<InputField>();
            btnLogin = skinTrans.Find("BtnLogin").GetComponent<Button>();
            btnReg = skinTrans.Find("BtnReg").GetComponent<Button>();


            btnLogin.onClick.AddListener(OnLoginClick);
            btnReg.onClick.AddListener(OnRegClick);
        }
        #endregion


        #region 按鈕監聽
        public void OnRegClick()
        {
            PanelMgr.Instance.OpenPanel<RegisterPanel>("");
            Close();
        }


        public void OnLoginClick()
        {
            //使用者名稱密碼為空
            if (inputFieldName.text == "" || inputFieldPwd.text == "")
            {
                PanelMgr.Instance.OpenPanel<WarningTip>("", "使用者名稱密碼不能為空");
                return;
            }


            ......
        }


        private void OnLoginBack(ProtocolBase protocol)
        {
            ProtocolBytes proto = (ProtocolBytes)protocol;
            int start = 0;
            proto.GetString(start, ref start);
            int ret = proto.GetInt(start, ref start);
            if (ret == 0)
            {
                PanelMgr.Instance.OpenPanel<WarningTip>("", "登陸成功");


                //進入遊戲主選單
                PanelMgr.Instance.OpenPanel<MenuButtonsPanel>("");
                Close();
            }
            else
            {
                PanelMgr.Instance.OpenPanel<WarningTip>("", "登入失敗");
            }
        }


        #endregion
    }
}


public override void Init(params object[] args) 中的args[]是方便在OpenPanel時傳引數的,比如警告Tip WarningTip
呼叫PanelMgr.Instance.OpenPanel<WarningTip>("", "登入失敗");就會顯示“登入失敗”
namespace UI.Tip
{
    public class WarningTip : PanelBase
    {
        private Text infoText;
        private Button ackButton;


        private string str = "";


        #region 宣告週期
        public override void Init(params object[] args)
        {
            base.Init(args);


            skinPath= "Tip/General/WarningTip";
            layer = PanelLayer.Tip;


            //要顯示的字元為args[1]
            if (args.Length == 1)
            {
                str = (string)args[0];
            }
        }


        public override void OnShowing()
        {
            base.OnShowing();


            Transform skinTrans = skin.transform;
            infoText = skinTrans.Find("TextInfo").GetComponent<Text>();
            ackButton = skinTrans.Find("BtnAck").GetComponent<Button>();


            infoText.text = str;
            ackButton.onClick.AddListener(OnBtnClick);
        }
        #endregion


        #region 按鈕監聽
        private void OnBtnClick()
        {
            Close();
        }
        #endregion
    }
}


其他的事就是在unity裡把介面的prefab製作好拖到對應的路徑裡就好啦,PanelMgr的layerDic利用的列舉型別的toString()也非常好拓展,程式碼分離之後也非常容易開發更多相關的功能。


最後寫一下Singletion<T>單例泛型的實現吧,感覺挺好用的,分享一下
public abstract class Singletion<T> : MonoBehaviour where T : MonoBehaviour
{
    public static string rootName = "MonoSingletionRoot";
    private static GameObject monoSingletionRoot;


    private static T instance;
    public static T Instance
    {
        get
        {
            if (monoSingletionRoot == null)
            {
                monoSingletionRoot = GameObject.Find(rootName);
                if (monoSingletionRoot == null) Debug.Log("please create a gameobject named " + rootName);
            }
            if (instance == null)
            {
                instance = monoSingletionRoot.GetComponent<T>();
                if (instance == null) instance = monoSingletionRoot.AddComponent<T>();
            }
            return instance;
        }
    }
}


當然monoSingletionRoot判空那裡其實也可以new GameObject(rootName)一下賦值過去啦,看個人喜好