Unity手遊框架 之 介面管理(一)
引子
介面管理是做什麼用的?為了回答這個問題讓我們先來羅列與介面操作相關的程式碼:
- 在網路訊息的回掉函式中我們發現了對介面的顯示、隱藏及遮擋關係調整的操作。
public static void OnPacket_UserLogon(object pack)
{
... ...
Debug.Log("Logon Succeed");
Gamedata.me.userinfo = userLogon.UserInfo;
PanelManager.me.HideAll(); // 介面的隱藏
PanelManager.me.Get((int)PanelId.PanelMain).Show(); // 介面的顯示
PanelManager.me.Get((int)PanelId.PanelMain).ToTop(); // 調整介面到最前方
... ...
}
- 在介面的初始化函式中我們發現了獲取介面控制元件的操作
public class PanelLogon : PanelBase
{
InputField username; // 使用者名稱
InputField password; // 密碼
public override void Init()
{
... ...
username = controls.GetInputField("InputFieldUsername");
password = controls.GetInputField("InputFieldPassword" );
... ...
}
}
- 在遊戲初始化階段,我們發現了為向PanelManager中註冊Prfab的操作。
PanelManager.me.RegistPrefab((int)PanelId.PanelLogon, AssetManager.me.GetPanel("PanelLogon"));
PanelManager.me.RegistPrefab((int)PanelId.PanelMain, AssetManager.me.GetPanel("PanelMain"));
- 在某介面中我們發現了事件處理函式。
void ProcessProgress(EventProgress evt)
{
int intProgress = (int)(evt.progress * 100);
txProcess.text = intProgress.ToString();
}
整理以上羅列出的程式碼,給出本文的介面管理的內容:
- 介面的顯示、隱藏、遮擋關係。
- 介面的載入與解除安裝。
- 介面控制元件的獲得。
- 介面中的事件響應。
- 除以上在程式碼中羅列的內容外,本文還加入了部分介面管理與熱跟新融合的部分內容。
本文的思路: 整理分析常見的介面管理的資料結構和遮擋關係管理方法,並給出本文采用的遮擋關係。整理分析常見的控制元件獲得方式,給出本文采用的控制元件獲取方式。講解PanelBase和PanelManager中的主要函式來理解介面的顯示、隱藏、載入及解除安裝的實現。介面中全域性事件的相應會放到事件系統中講解,最後附錄中給出本文的完整的原始碼。
介面管理的資料結構和遮擋關係的管理方法
介面管理的資料結構一般有兩中:棧結構和對結構。
* 棧結構為基礎的介面管理。
在這種方式中介面的顯示相當與如棧,而介面的隱藏則相當於出棧。這種方式的好處是結構清晰、遮擋關係的管理簡單並天生就有介面顯示流的逆向追蹤能力。當顯示一個介面時,將這個介面放入棧頂並顯示,處於棧頂的介面永遠處於遮擋關係的最前方。當隱藏一個介面是將從這個介面到棧頂的介面一律出棧並隱藏。這種方式的不足是經常不能滿足策劃對界管理的要求,於是加入各種補丁使得原本清晰簡單的結構變的複雜而混亂。
* 以堆結構為基礎的介面管理。
這種方式在實現遮擋關係管理上要比棧結構複雜,要實現諸如ToTop、ToBottom、MoveForward、MoveBackward、SetQueueIndex等控制遮擋關係的函式。
本文實現的是閹割版的堆結構為基礎的介面管理,即僅實現了眾多遮擋控制函式中的ToTop,在使用上顯示即ToTop。另外,有些複雜的介面管理還實現了介面顯示流的逆向追蹤,這個也不在本文的討論範圍。
介面中控制元件的取得
常見的控制元件獲取的三種方式
* 拖拽式。 這種方式大概是這樣實現的, 首先在程式碼中宣告控制元件變數,在製作Panel的prefab時將相應的控制元件拖拽到prefab上。 程式碼中大概是這樣的:
class PanelHero : Behaviour
{
public Button btnUpLevel;
public Button btnUpStar;
public void Awake()
{
btnUpLevel.onClick.AddListener(OnUpLevelClick);
btnUpStar.OnClick.AddListener(OnUpStarClick);
}
... ...
}
- 路徑查詢式。這中方式與第一中方式類似, 但在製作介面的Prefab時不需要將控制元件拖拽到相應的變數上,在載入介面是由程式通過路徑查詢的方式找到相應的空間, 程式碼大概是這樣的。
class PanelHero : Behaviour
{
public Button btnUpLevel;
public Button btnUpStar;
public void Awake()
{
btnUpLevel = transform.Find("HeroLevel/ButtonUpLevel").GetComponent<Button>();
btnUpStar = trnaform.Find("HeroQualityAndStar/ButtonUpStar").GetComponent<Button>();
btnUpLevel.onClick.AddListener(OnUpLevelClick);
btnUpStar.OnClick.AddListener(OnUpStarClick);
}
... ...
}
- 註冊式。 這種方式需要兩個類, Register和Container, 在製作控制元件時將Container置於Panel上, 將Register置於要與程式產生關聯的控制元件上, 在程式中使用使用首先得到Container,然後在Container中取得相應的控制元件。 程式碼如下
// 控制元件註冊器
class UiControlRegistor : MonoBehaviour
{
// 指定的目標容器
public UiControlContainer PointedContainer = null;
public virtual void Awake()
{
container.RegistObject(name, gameObject); // 註冊本控制元件
}
// 得到控制元件的目標容器
public UiControlContainer container
{
// 如果制定了目標容器就返回制定的目標容器,
// 否則依次向跟節點查詢容器, 返回路徑上的第一個容器作為目標容器
get { return PointedContainer ? PointedContainer :
transform.GetComponentInParent<UiControlContainer>(); }
}
}
// 控制元件容器
class UiControlContainer : MonoBehaviour
{
// GameObject 型別的容器
Dictionary<string, GameObject> objects = new Dictionary<string, GameObject>();
// 泛型的控制元件註冊函式
bool RegistT<T>(Dictionary<string, T> datas, string name, T unit)
{
if (datas.ContainsKey(name))
{
Debug.LogError("name(" + name + ") already exist");
return false;
}
datas.Add(name, unit);
return true;
}
// GameObject型別的控制元件註冊函式
public bool RegistObject(string name, GameObject obj)
{
return RegistT(objects, name, obj);
}
... ...
}
下面給出這三種方案的對比:
第一種方案(拖拽式)的優點, 簡潔,直觀,效率高。缺點prefab與對應的指令碼關聯行強,難於分離指令碼開發與perfab製作,很難支援lua熱更新。
第二種方案(路徑查詢式)的優點,簡潔,直觀, prefab的製作不再依賴與指令碼開發。缺點查詢的效率低,指令碼開發依舊依賴與prefab製作。這種方案可以將prefab的製作人員與指令碼開發人員分離,但每次修改prefab後腳本依舊要緊隨其後的修改。這種方案可以支援Lua熱跟新。
第三種方案(註冊式)優點prefab的製作與指令碼開發很好的分離,很好的支援lua熱跟新。效率介於前兩種方案之間,缺點是結構稍顯複雜。本文采用的方案是第三種方案。
PanelBase的主要函式
PanelBase是介面的基類, 主要的介面有Init,Show, Hide, Refresh,與層次相關的介面前面已經提到過了,本文采用的是閹割版,因此在這個類中看不到遮擋關係系列的函式。對於介面控制元件的響應函式應該放到具體的Panel裡,對於全域性事件的相應放到事件系統中講解。
/// <summary>
/// 介面的基類
/// 定義了諸如Init、Show、Hide、Refresh等可重寫的流程函式
/// </summary>
[RequireComponent(typeof(UiControlContainer))]
public class PanelBase : MonoBehaviour
{
/// <summary>
/// 控制元件容器
/// </summary>
public UiControlContainer controls { get { return GetComponent<UiControlContainer>(); } }
/// <summary>
/// 所有SubPanel
/// </summary>
public PanelBase[] subPanels { get { return controls.panelbases.Values.ToArray<PanelBase>(); } }
/// <summary>
/// 根據名字獲取Button控制元件
/// </summary>
/// <param name="name">Button的名字</param>
/// <returns>Button控制元件</returns>
public virtual void Init()
{
foreach (PanelBase subPanel in subPanels)
{
bool active = gameObject.activeSelf;
gameObject.SetActive(true);
foreach (PanelBase subPanel in subPanels)
{
subPanel.Init();
}
gameObject.SetActive(active);
}
}
/// <summary>
/// 顯示介面
/// </summary>
public virtual void Show()
{
gameObject.SetActive(true);
}
/// <summary>
/// 隱藏介面
/// </summary>
public virtual void Hide()
{
gameObject.SetActive(false);
}
/// <summary>
/// 重新整理介面
/// </summary>
public virtual void Refresh()
{
}
}
PanelManager的主要函式
PanelManager是介面管理的核心類, 主要函式有RegistPrefab、Get、HideAll、RefreshAll等
public class PanelManager : Singleton<PanelManager>
{
// 介面的例項化函式
public delegate PanelBase FuncInstanceUiBase(PanelBase prefab);
public FuncInstanceUiBase InstanceUiBase;
// prefab 容器
private readonly Dictionary<int, PanelBase> prefabs = new Dictionary<int, PanelBase>();
// 已經載入的介面容器
private readonly Dictionary<int, PanelBase> uis = new Dictionary<int, PanelBase>();
/// <summary>
/// 註冊介面的Prefab
/// </summary>
/// <param name="uiid">介面的型別id</param>
/// <param name="prefab">介面的prefab</param>
public void RegistPrefab(int uiid, PanelBase prefab)
{
prefabs.Add(uiid, prefab);
}
/// <summary>
/// 獲取介面
/// </summary>
/// <param name="uiid">介面的型別id</param>
/// <returns></returns>
public PanelBase Get(int uiid)
{
if (uis.ContainsKey(uiid))
return uis[uiid];
if (prefabs.ContainsKey(uiid))
{
var ui = InstanceUiBase(prefabs[uiid]);
uis.Add(uiid, ui);
ui.gameObject.SetActive(true);
ui.name = ui.name.Substring(0, ui.name.IndexOf("(Clone)"));
ToTop(ui);
ui.Init();
AdjustPanelPosition(ui);
}
return null;
}
/// <summary>
/// 隱藏所有介面
/// </summary>
public void HideAll()
{
foreach (var ui in uis.Values)
ui.Hide();
}
/// <summary>
/// 顯示所有介面
/// </summary>
public void RefreshAll()
{
foreach (var ui in uis.Values)
ui.Refresh();
}
}
小結
本文從常見的介面相關的原始碼出發,提出了介面管理的基礎功能,並重點講解了介面控制元件的獲得和介面管理的資料結構。有關介面管理的高階部分,例如介面的過度動畫,介面切換觸發的功能(例如新手引導),介面的導航與追蹤等部分將在下一篇介面管理的文章中給出。
附:完整原始碼
// PanelBase.cs
// Author: Iann
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using System.Reflection;
namespace Assets.Script.Frame
{
/// <summary>
/// 介面的基類
/// 定義了諸如Init、Show、Hide、Refresh等可重寫的流程函式
/// </summary>
[RequireComponent(typeof(UiControlContainer))]
public class PanelBase : MonoBehaviour
{
/// <summary>
/// 控制元件容器
/// </summary>
public UiControlContainer controls { get { return GetComponent<UiControlContainer>(); } }
/// <summary>
/// 所有SubPanel
/// </summary>
public PanelBase[] subPanels { get { return controls.panelbases.Values.ToArray<PanelBase>(); } }
/// <summary>
/// 根據名字獲取Button控制元件
/// </summary>
/// <param name="name">Button的名字</param>
/// <returns>Button控制元件</returns>
public virtual Button GetButton(string name)
{
return controls.GetButton(name);
}
/// <summary>
/// 根據名字獲取Button控制元件
/// </summary>
/// <param name="name">Button的名字</param>
/// <returns>Button控制元件</returns>
public virtual Text GetText(string name)
{
return controls.GetText(name);
}
/// <summary>
/// 根據名字獲取Image控制元件
/// </summary>
/// <param name="name">Image的名字</param>
/// <returns>Image控制元件</returns>
public virtual Image GetImage(string name)
{
return controls.GetImage(name);
}
/// <summary>
/// 根據名字獲取SubPanel控制元件
/// </summary>
/// <param name="name">SubPanel的名字</param>
/// <returns>SubPanel控制元件</returns>
public virtual PanelBase GetSubPanel(string name)
{
return controls.GetPanelBase(name);
}
/// <summary>
/// 介面的初始化函式
/// 一般過載這個函式的主要目的是初始化介面控制元件
/// 注意控制元件的註冊是在Awake函式中進行的,保證在呼叫container.get的時候控制元件註冊器的Awake已經呼叫
/// </summary>
public virtual void Init()
{
#if USE_LOG && LOG_FRAME_UI
Debug.Log("Init Panel " + name + ".");
#endif
bool active = gameObject.activeSelf;
gameObject.SetActive(true); // 確保初始化前UiControlRegister的Awake已經呼叫
foreach (PanelBase subPanel in subPanels)
{
subPanel.Init();
}
gameObject.SetActive(active);
}
/// <summary>
/// 顯示介面
/// </summary>
public virtual void Show()
{
#if USE_LOG && LOG_FRAME_UI
Debug.Log("PaneBase(" + name + ").Show");
#endif
gameObject.SetActive(true);
//OnShow();
Refresh();
}
/// <summary>
/// 隱藏介面
/// </summary>
public virtual void Hide()
{
#if USE_LOG && LOG_FRAME_UI
Debug.Log("PaneBase(" + name + ").Hide");
#endif
gameObject.SetActive(false);
}
/// <summary>
/// 重新整理介面
/// </summary>
public virtual void Refresh()
{
#if USE_LOG && LOG_FRAME_UI
Debug.Log("PanelBase(" + name + ").Refresh");
#endif
}
}
}
// PanelManager.cs
// Author: Iann
using System.Collections.Generic;
using UnityEngine;
namespace Assets.Script.Frame
{
/// <summary>
/// 介面管理類
/// 本類提供了介面的載入,顯示,隱藏,(關閉)功能
/// 動畫過度暫未實現
/// 導航功能暫未實現
/// </summary>
public class PanelManager : Singleton<PanelManager>
{
// 介面的例項化函式
public delegate PanelBase FuncInstanceUiBase(PanelBase prefab);
public FuncInstanceUiBase InstanceUiBase;
// prefab 容器
private readonly Dictionary<int, PanelBase> prefabs = new Dictionary<int, PanelBase>();
// 已經載入的介面容器
private readonly Dictionary<int, PanelBase> uis = new Dictionary<int, PanelBase>();
public void Regist(int uiid, PanelBase ui)
{
//types.Add(typeof(T), uiid);
uis.Add(uiid, ui);
}
/// <summary>
/// 註冊介面的Prefab
/// </summary>
/// <param name="uiid">介面的型別id</param>
/// <param name="prefab">介面的prefab</param>
public void RegistPrefab(int uiid, PanelBase prefab)
{
if (prefab == null)
{
Debug.LogError(string.Format("PanelManager.RegistPrefab({0},{1}) Failed !!!",
uiid, prefab));
return;
}
#if USE_LOG
Debug.Log(string.Format("PanelManager.RegistPrefab({0},{1}", uiid, prefab));
#endif
prefabs.Add(uiid, prefab);
}
/// <summary>
/// 獲取介面
/// </summary>
/// <param name="uiid">介面的型別id</param>
/// <returns></returns>
public PanelBase Get(int uiid)
{
//如果介面已經載入過, 返回已經載入過的介面
if (uis.ContainsKey(uiid))
return uis[uiid];
// 載入介面
if (prefabs.ContainsKey(uiid))
{
if (prefabs[uiid] == null)
{
Debug.LogError((PanelId)uiid);
}
var ui = InstanceUiBase(prefabs[uiid]);
uis.Add(uiid, ui);
ui.gameObject.SetActive(true); // 確保初始化前UiControlRegister的Awake已經呼叫
ui.name = ui.name.Substring(0, ui.name.IndexOf("(Clone)"));
ToTop(ui);
ui.Init();
AdjustPanelPosition(ui);
return ui;
}
return null;
}
/// <summary>
/// 調整Panel的位置佈局
/// </summary>
/// <param name="ui">待調整的Panel</param>
void AdjustPanelPosition(PanelBase ui)
{
ui.GetComponent<RectTransform>().SetParent(GameObject.Find("Canvas").transform);
var rt = ui.GetComponent<RectTransform>();
var op = ui.GetComponent<RectTransformAdjustor>();
if (op != null)
{
op.Adjust();
}
else
{
ui.transform.localPosition = Vector3.zero;
ui.transform.localScale = Vector3.one;
}
}
/// <summary>
/// 將Panel放到最前面
/// </summary>
/// <param name="ui">待調整的Panel</param>
void ToTop(PanelBase ui)
{
ui.transform.SetAsLastSibling();
}
/// <summary>
/// 隱藏所有介面
/// </summary>
public void HideAll()
{
foreach (var ui in uis.Values)
ui.Hide();
}
/// <summary>
/// 顯示所有介面
/// </summary>
public void RefreshAll()
{
foreach (var ui in uis.Values)
ui.Refresh();
}
}
}
// UiControlContainer.cs
// Author: Iann
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
namespace Assets.Script.Frame
{
public class UiControlContainer : MonoBehaviour
{
private readonly Dictionary<string, Button> buttons = new Dictionary<string, Button>();
private readonly Dictionary<string, Image> images = new Dictionary<string, Image>();
private readonly Dictionary<string, InputField> inputs = new Dictionary<string, InputField>();
private readonly Dictionary<string, Text> texts = new Dictionary<string, Text>();
private readonly Dictionary<string, Slider> sliders = new Dictionary<string, Slider>();
private readonly Dictionary<string, GameObject> objects = new Dictionary<string, GameObject>();
public Dictionary<string, PanelBase> panelbases = new Dictionary<string, PanelBase>();
private bool RegistT<T>(Dictionary<string, T> datas, string name, T unit)
{
if (datas.ContainsKey(name))
{
Debug.LogError("name(" + name + ") already exist");
return false;
}
datas.Add(name, unit);
return true;
}
private bool UnRegistT<T>(Dictionary<string, T> datas, string name)
{
if (!datas.ContainsKey(name))
{
Debug.LogError("name not exist");
return false;
}
datas.Remove(name);
return true;
}
// Object
public bool RegistObject(string name, GameObject obj)
{
return RegistT(objects, name, obj);
}
public bool UnRegistObject(string name)
{
return UnRegistT(objects, name);
}
public GameObject GetObject(string name)
{
if (objects.ContainsKey(name))
return objects[name];
return null;
}
// Button
public bool RegistButton(string name, Button button)
{
return RegistT(buttons, name, button);
}
public bool UnRegistButton(string name)
{
return UnRegistT(buttons, name);
}
public Button GetButton(string name)
{
if (buttons.ContainsKey(name))
return buttons[name];
return null;
}
// Text
public bool RegistText(string name, Text text)
{
return RegistT(texts, name, text);
}
public bool UnRegistText(string name)
{
return UnRegistT(texts, name);
}
public Text GetText(string name)
{
if (texts.ContainsKey(name))
return texts[name];
return null;
}
// Slider
public bool RegistSlider(string name, Slider slid)
{
return RegistT(sliders, name, slid);
}
public bool UnRegistSlider(string name)
{
return UnRegistT(sliders, name);
}
public Slider GetSlider(string name)
{
if (sliders.ContainsKey(name))
return sliders[name];
return null;
}
// Image
public bool RegistImage(string name, Image image)
{
return RegistT(images, name, image);
}
public bool UnRegistImage(string name)
{
return UnRegistT(images, name);
}
public Image GetImage(string name)
{
if (images.ContainsKey(name))
return images[name];
return null;
}
// ImputFiled
public bool RegistInputField(string name, InputField inputFiled)
{
return RegistT(inputs, name, inputFiled);
}
public bool UnRegistInputField(string name)
{
return UnRegistT(inputs, name);
}
public InputField GetInputField(string name)
{
if (inputs.ContainsKey(name))
return inputs[name];
return null;
}
// PanelBase
public bool RegistPanelBase(string name, PanelBase panelBase)
{
return RegistT(panelbases, name, panelBase);
}
public bool UnRegistPanelBase(string name)
{
return UnRegistT(panelbases, name);
}
public PanelBase GetPanelBase(string name)
{
if (panelbases.ContainsKey(name))
return panelbases[name];
return null;
}
}
}
// UiControlRegistor.cs
// Author: Iann
using UnityEngine;
namespace Assets.Script.Frame
{
/// <summary>
/// 介面控制元件註冊器的基類
/// </summary>
class UiControlRegistor : MonoBehaviour
{
/// <summary>
/// 指定的介面控制元件容器
/// </summary>
public UiControlContainer PointedContainer = null;
/// <summary>
/// 註冊後立即刪除本註冊器, 用於多次載入的選項
/// </summary>
public bool removeThisWhenAwake = false;
/// <summary>
/// 喚醒函式, 我們的註冊就是在這個函式中實現的
/// </summary>
public virtual void Awake()
{
#if USE_LOG && LOG_FRAME_UI
Debug.Log("UiBehaviour(" + name + ").Awake");
#endif
container.RegistObject(name, gameObject);
if (removeThisWhenAwake)
{
DestroyImmediate(this);
}
}
/// <summary>
/// 銷燬函式, 反註冊寫在這個函式中
/// </summary>
public virtual void Destroy()
{
#if USE_LOG && LOG_FRAME_UI
Debug.Log("UiBehaviour(" + name + ").Destroy");
#endif
if (!removeThisWhenAwake)
container.UnRegistObject(name);
}
/// <summary>
/// 如果制定了目標容器就返回制定的目標容器,
/// 否則依次向跟節點查詢容器, 返回路徑上的第一個容器作為目標容器
/// </summary>
public UiControlContainer container { get { return PointedContainer ?
PointedContainer : transform.GetComponentInParent<UiControlContainer>(); } }
}
}
// UiControlContainer.cs
// Author: Iann
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
namespace Assets.Script.Frame
{
/// <summary>
/// 控制元件容器
/// </summary>
public class UiControlContainer : MonoBehaviour
{
//各種型別的器皿
private Dictionary<string, Button> buttons = new Dictionary<string, Button>();
private Dictionary<string, Image> images = new Dictionary<string, Image>();
private Dictionary<string, InputField> inputs = new Dictionary<string, InputField>();
private Dictionary<string, Text> texts = new Dictionary<string, Text>();
private Dictionary<string, Slider> sliders = new Dictionary<string, Slider>();
private Dictionary<string, GameObject> objects = new Dictionary<string, GameObject>();
private Dictionary<string, PanelBase> panelbases = new Dictionary<string, PanelBase>();
/// <summary>
/// 泛型的註冊函式
/// </summary>
/// <typeparam name="T">註冊的型別</typeparam>
/// <param name="datas">註冊用到的器皿</param>
/// <param name="name">控制元件名稱</param>
/// <param name="unit">控制元件Instance</param>
/// <returns></returns>
private bool RegistT<T>(Dictionary<string, T> datas, string name, T unit)
{
if (datas.ContainsKey(name))
{
Debug.LogError("name(" + name + ") already exist");
return false;
}
datas.Add(name, unit);
return true;
}
/// <summary>
/// 泛型的反註冊函式
/// </summary>
/// <typeparam name="T">反註冊的型別</typeparam>
/// <param name="datas">反註冊用到的器皿</param>
/// <param name="name">控制元件名稱</param>
/// <returns></returns>
private bool UnRegistT<T>(Dictionary<string, T> datas, string name)
{
if (!datas.ContainsKey(name))
{
Debug.LogError("name not exist"<