1. 程式人生 > >Unity3D 設計模式學習之組合模式

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類的成員。
UML類圖

類結構圖如上= = 就如同遊戲系統那樣,對內可以通過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中新增查詢失敗的警告。