unity介面和程式碼分離解決方案
阿新 • • 發佈:2019-01-31
新手或者小規模的遊戲喜歡使用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)一下賦值過去啦,看個人喜好