Unity3D 設計模式學習之組合模式
前言
對應遊戲中的UI部分,可以使用分層式管理,將2D元件按照功能關係按層次擺放,是比較容易瞭解、設計和修改的。
分層式管理一般也稱樹狀結構,常用於軟體實現和應用中的一種結構。
GOF對於組合模式Composite的定義是:
“將物件以樹狀結構組合,用以表現部分-全體的層次關係。組合模式讓客戶端在操作 各個物件或組合和組合物件是一致的”
樹狀結構來組合各個物件,所以實現上包含了根結點和葉結點的概念。而”根節點”中會包含葉節點的物件,所以當根節點被刪除時,葉節點也會被一起刪除,並且希望對於”根節點”和“葉節點”在操作方法上能夠一致。
這表示,這兩種節點都是繼承自同一個操作介面,能夠對根節點呼叫的操作,同樣能在葉節點上使用。
遊戲物件中的分層管理
在Unity中每個GameObject物件都有一個Transform元件,這個元件提供了幾個和遊戲物件分層操作有關的方法和變數。
變數:
childCount:代表子元件數量
parent:代表父元件中的Transform物件引用
方法:
DetachChildren:解除所有子元件與本身的關聯
Find: 尋找子元件
GetChild: 使用Index的方式取回子元件
IsChildOf: 判斷某個Transform物件是否為其子元件
SetParent:設定某個Transform物件為其父元件
再仔細分析,則可以將Unity3D的Transform類當成是一個通用類,因為它並不明顯得可以察覺出其下又被再分成“目錄節點”或是單純的“單的終端節點”
其實應該說,Transform類完全符合組合模式的需求:“讓客戶端在操作各個物件或元件時是一致的”。
因此對於場景上所有的遊戲物件GameObject,可以不管它們最終代表的什麼,對於所有操作都能正確反應。
遊戲使用者介面
在《P級陣地》中,每一個主要遊戲功能屬於IGameSystem的子類,這些子類負責實現《P級陣地》中不同的遊戲需求和功能,而它們每一個都會利用組合的方式,成為PBaseDefenseGame類的成員。
public abstract class IUserInterface{
protected PBaseDefenseGame m_PBDGame = null ;
protected GameObject m_RootUI = null;
private bool m_bActive = true;
public IUserInterface(PBaseDefenseGame PBDGame){
m_PBDGame = PBDGame;
}
public bool IsVisible(){
return m_bActive;
}
public virtual void Show(){
m_RootUI.SetActive(true);
m_bActive = true;
}
public virtual void Hide(){
m_RootUI.SetActive(false);
m_bActive = false;
}
public virtual void Initalize(){}
public virtual void Release(){}
public virtual void Update(){}
}
遊戲中使用的4個介面偶是IUseInterface的子類,並且使用組合的方式成為PBaseDefenseGame類的成員。
類結構圖如上= = 就如同遊戲系統那樣,對內可以通過PBaseDefenseGame類中的中介者模式來通知其他系統或介面,對外也可以通過PBaseDenfenseGame類的外觀模式讓客戶端存取和更新使用者介面相關的功能
兵營介面的實現
public class CampInfoUI:IUserInterface{
//...介面元件
private Text m_AliveCountTxt = null;
private Text m_CampLvTxt = null;
private Text m_WeaponLvTxt = null;
private Image m_CampImage = null;
public CampInfoUI(PBaseDefenseGame PBDGame):base(PBDGame){
Initialize();
}
public override void Initialize(){
m_RootUI = UITool.FindUIGameObject("CampInfoUI");
//顯示的資訊
//兵營名稱
m_CampNameTxt = UITool.GetUIComponent<Text>(m_RootUI,"CampNameText");
//兵營圖
m_CampImage = UITool.GetUIComponent<Image>(m_RootUI,"CampIcon");
}
public void ShowInfo(Icamp Camp){
Show();
m_Camp = Camp;
//名稱
m_CampNameTxt.text = m_Camp.GetName();
//...以下都為顯示UI資訊
}
}
這裡的UI指令碼並有繼承MonoBehaviour 而是繼承UI介面,通過建構函式來初始化UI指令碼,
然後定義一個公有Show方法來做顯示和更新
這裡還自定義了UI有關的查詢工具類,以便查詢UI物體指令碼。
public static class UITool{
//場景上的2D畫布物件
private static GameObject m_CanvasObj = null;
public static GameObject FindUIGameObject(string UIName){
if(m_CanvasObj == null)
m_CanvasObj = UnityTool.FindGameObject("Canvas");
if(m_CanvasObj == null)
return null;
return UnityTool.FindChildGameObject(m_CanvasObj,UIName);
}
public static T GetUIComponent<T>(GameObject Container,string UIName) where T : UnityEngine.Component {
//找出子物件
GameObject childGameObject = UnityTool.FindChildGameObject(Container,UIName);
if(ChildGameObject == null)
return null;
T tempObj = ChildGameObj.GetComponent<T>();
if(tempObj == null){
Debug.LogWarning("元件["+UIName+"]不是["+typeof(T)+"]");
}
return tempObj;
}
}
以上是UI工具類
UI工具類用到了自定義的UnityTool工具類
public static class UnityTool{
//找到場景上的物件
public static GameObject FindGameObject(string GameObjectName){
//查詢場景上的物件
GameObject pTmpGameObj = GameObject.Find(GameObjectName);
if(pTmpGameObj == null) {
Debug.LogWarning("場景中找不到GameObject["+GameObjectName+"]物件");
return null;
}
return pTmpGameObj;
}
public static GameObject FindChildGameObject(GameObject Container,string gameObjectName){
if(Container == null){
Debug.LogError("無父類物件");
return null;
}
Transform pGameObjectTF = null;
//是不是Container本身
if(Container.name == gameobjectName)
pGameObjectTF = Container.transform;
else{
Transfrom [] allChildren = Container.transform.GetComponentsInChildren<Transform>();
foreach(Transform child in allChildren){
if(child.name== gameObjectName)
if(pGameObject == null)
pGameObjectTF = child;
else
Debug.LogWarning("Container["+Container.name+"]下找出重子元件名稱["+gameObjectName+"]");
}
if(GameObjectTF == null){
Debug.LogError("元件["+Container.name+"]找不到子元件["+gameObjectName+"]");
return null;
}
return pGameObjectTF.gameObject;
}
}
UnityTool工具類中,FindChildGameObject 方法是用來搜尋某遊戲物件下的子物件。
對重複命名的問題加以防呆(防止出錯的處理)以便在開發時查詢除錯。
並且因為是這查詢中使用,雖然需要遍歷一遍,但是元件可以只獲取一次,在後期遊戲執行狀態下並不會一直使用搜索介面元件的功能。
這也是大家常說的:不要頻繁呼叫GetComponent方法獲得元件,一般在Awake Start函式獲取一遍。
m_CampNameTxt = UIToo.GetUIComponent<Text>(m_RootUI,"CampNameText");
接下來 CampInfoUI有一個公有ShowInfo方法,根據功能需求來控制顯示UI資訊
public void ShowInfo(ICamp camp){
Show();
m_Camp = camp;
m_CampNameTxt.text = m_Camp.GetName();
m_TrainConstText.text = string.Format("AP:{0}",m_Camp.GetTrainCost());
//訓練中資訊
ShowOnTranInfo();
//Icon
IAssetFactory Factory = PBDFactory.GetAssetFactory();
m_CampImage.sprite = Factory.LoadSprite(m_Camp.GetIconSpriteName());
if(m_Camp.GetLevel() <= 0) {
EnableLevelInfo(false);
}else{
m_CampLevelInfo(true);
m_CampLvTxt.text = string.Format("等級:"+m_Camp.GetLevel());
m_WeaponLvTxt.text = string.Format("武器等級:"+m_Camp.GetWeaponLevel());
}
}
總結
使用組合模式的優點:
介面與功能分離,更具移植性
工作切分更容易,當指令碼移除,就可以讓UI設計交由美術和企劃組裝
介面更改不影響專案:只要維持元件名稱不變,介面的更改就不容易影響到遊戲現有程式功能的執行。
缺點:
元件名稱重複,如果沒有將層級切分好,就容易出現該問題,在工具名稱新增警告可以解決。
元件更名不易:元件名需要通過字串來查詢,介面元件一旦不能獲取,則會出現null值,和不正確的場景,應對的方法同樣是在UnityTool中新增查詢失敗的警告。