unity 網絡遊戲架構設計(第04課:UI 架構設計)之美

第04課:UI 架構設計

前面的內容是作爲架構底層設計,基本上每一款遊戲都會這麼做的,只是封裝的方式不同罷了。下面開始實現 UI 架構設計,在每款遊戲中都會涉及到 UI 邏輯編寫,而且 UI 還有一個特點就是資源更換非常頻繁,程序在做 UI 架構設計時,這些問題也是要重點考慮的。

市面上也有一些關於 UI 的插件,如 psd2ugui、psd2ngui 等,這些插件美術使用起來非常方便,直接把 psd 源文件給 Unity,然後 Unity 利用上述插件,生成所需要的 UI 和圖集,psd 中的圖層在 Unity 同樣也可以實現,但是它們是作爲一個整體去處理的,這樣一旦某些部件需要更換,那整個 psd 都需要更換,如果 UI 資源需求量比較少,可以直接使用該插件,但是如果 UI 資源量大,再加上 UI 頻繁的更換,這樣的遊戲邏輯就會出現各種問題,比如名字不對了,圖層的的設置對應着 Unity 中的 Layer Order 設置,psd 中的命名變換了等等,這些因素都不利於程序員維護。爲此我們需要提供新的 UI 框架設計,先把模塊架構設計拿出來給讀者展示如下所示:

enter image description here

先說說爲什麼這麼設計?UI 的設計框架很多的,本課程的 UI 框架不能說是最好的,但是它已經經過了商業遊戲的驗證,是可行的。它最大的優點就是實現了資源和代碼的徹底分離,真正實現了程序和美術人員分工合作。我們的 UI 框架也是採用了 MVC 設計模式,但是在外面加了一層 State 狀態變換,因爲每個 UI 切換是不同的狀態之間的變換,這樣整個 UI 架構設計就完成了,剩下的事情就是細節的完善了,也是具體工作的開始。

UI 窗體的設計

遊戲的每個 UI 使用 Panel 面板製作完成,每個面板就是一個窗體,用 MVC 中的 View 表示,用代碼表示就是 Window 窗體類,MVC 中的 Control 也同樣對應窗體的 Control 類。MVC 中的 Model 表示的是數據的傳輸,在當前可以直接使用配置文件進行讀取加載,或者通過網絡進行傳輸,它在本課程的 UI 設計中 Model 地位不明顯,在此就不介紹了。

先說說 Window 窗體類的設計,每個 UI 對應自己的 Window 窗體類。下面先介紹 Window 類模塊的代碼編寫,在寫某個模塊時,首先做的事情是搞清楚這個模塊包括哪些內容,再考慮一下編寫此類時擴展是否方便。這些都是編寫程序注意的問題,有的程序員拿過策劃需求就開始編寫代碼,這樣導致的後果是一旦策劃需求改變,代碼就要重新加功能,搞的很被動,這對於程序員是大忌。

以 Window 類編寫爲例,思考一下,遊戲中顯示窗體首先要創建窗體,還有窗體可以隱藏、銷燬,這是最基本的功能。另外,我們創建窗體首先要知道窗體的資源名字,還有這個窗體是在哪個場景中創建的,是登錄場景還是遊戲戰鬥場景等。因爲窗體類不繼承 Mono,爲了方便使用窗體中的控件,所以還要做初始化窗體控件的功能以及做監聽處理等。

我們說的這些功能對於遊戲中的任何 UI 窗體都是適用的,換句話說,所有的 UI 這些功能都是必備的,也是 UI 的共性,這讓人自然而然想到建一個父類,如果不建父類,每個 Window 類都要寫一套邏輯,這會導致代碼很亂,而且如果是多個人協作寫,每人再來一套邏輯,後期代碼無法維護。所以必須要建一個父類,其實編程沒有那麼難的,需要自己多想想就明白了,下面開始具體編寫窗體父類代碼,代碼如下:

    public abstract class BaseWindow
{
    protected Transform mRoot; //UI根結點

    protected EScenesType mScenesType; //場景類型
    protected string mResName;         //資源名
    protected bool mResident;          //是否常駐 
    protected bool mVisible = false;   //是否可見


    //類對象初始化
    public abstract void Init();

    //類對象釋放
    public abstract void Realse();

    //窗口控制初始化
    protected abstract void InitWidget();

    //窗口控件釋放
    protected abstract void RealseWidget();

    //遊戲事件註冊
    protected abstract void OnAddListener();

    //遊戲事件注消
    protected abstract void OnRemoveListener();

    //顯示初始化
    public abstract void OnEnable();

    //隱藏處理
    public abstract void OnDisable();

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

    //取得所以場景類型
    public EScenesType GetScenseType()
    {
        return mScenesType;
    }

    //是否已打開
    public bool IsVisible() { return mVisible;  }

    //是否常駐
    public bool IsResident() { return mResident; }

    //顯示
    public void Show()
    {
        if (mRoot == null)
        {
            if (Create())
            {
                InitWidget();
            }
        }

        if (mRoot && mRoot.gameObject.activeSelf == false)
        {
            mRoot.gameObject.SetActive(true);

            mVisible = true;

             OnEnable();

            OnAddListener();
        }
    }

    //隱藏
    public void Hide()
    {
        if (mRoot && mRoot.gameObject.activeSelf == true)
        {
            OnRemoveListener();
            OnDisable();

            if (mResident)
            {
                mRoot.gameObject.SetActive(false);
            }
            else
            {
                RealseWidget();
                Destroy();
            }
        }

        mVisible = false;
    }

    //預加載
    public void PreLoad()
    {
        if (mRoot == null)
        {
            if (Create())
            {
                InitWidget();
            }
        }
    }

    //延時刪除
    public void DelayDestory()
    {
        if (mRoot)
        {
            RealseWidget();
            Destroy();
        }
    }

    //創建窗體
    private bool Create()
    {
        if (mRoot)
        {
            Debug.LogError("Window Create Error Exist!");
            return false;
        }

        if (mResName == null || mResName == "")
        {
            Debug.LogError("Window Create Error ResName is empty!");
            return false;
        }

        if (GameMethod.GetUiCamera.transform== null)
        {
            return false;
        }

        GameObject obj = LoadUiResource.LoadRes(GameMethod.GetUiCamera.transform, mResName);

        if (obj == null)
        {
            return false;
        }

        mRoot = obj.transform;

        mRoot.gameObject.SetActive(false);

        return true;
    }

    //銷燬窗體
    protected void Destroy()
    {
        if (mRoot)
        {
            LoadUiResource.DestroyLoad(mRoot.gameObject);
            mRoot = null;
        }
    }

    //取得根節點
    public Transform GetRoot()
    {
        return mRoot;
    }

}

在關鍵代碼片段地方都加了註釋,我們的父類設計成了一個抽象類並提供了一些接口便於子類實現,這些接口所要實現的具體內容是不同的,父類無法具體一一實現,但是顯示、隱藏、破壞這些都是通用的函數,可以在父類中實現。再看看子類的實現方式,以 LoginWindow 類爲例,我們製作一個簡單的 UI 界面,如下圖所示:

enter image description here

在 Unity 的顯示如下所示:

enter image description here

UI 中有兩個 Button 按鈕,我們就以此爲例進行代碼編寫,如下所示:

 public class LoginWindow : BaseWindow
{
    //開始
    Transform mBtnStart;

    enum LOGINUI
    {
        None = -1,
        Login,
        SelectServer,
    }

    public LoginWindow() 
    {
        //場景類型
        mScenesType = EScenesType.EST_Login;
        //場景資源
        mResName = GameConstDefine.LoadGameLoginUI;
        //是否常駐內存
        mResident = false;
    }

    ////////////////////////////繼承接口/////////////////////////
    //類對象初始化監聽顯示和隱藏,爲了解耦合
    public override void Init()
    {
        EventCenter.AddListener(EGameEvent.eGameEvent_LoginEnter, Show);
        EventCenter.AddListener(EGameEvent.eGameEvent_LoginExit, Hide);
    }

    //類對象釋放
    public override void Realse()
    {
        EventCenter.RemoveListener(EGameEvent.eGameEvent_LoginEnter, Show);
        EventCenter.RemoveListener(EGameEvent.eGameEvent_LoginExit, Hide);
    }

    //窗口控件初始化以及控件監聽
    protected override void InitWidget()
    {
        mBtnStart = mRoot.Find("BtnStart");
        mBtnStart.GetComponent<Button>().onClick.AddListener(OnClickAddButton);

        DestroyOtherUI();
    }

    //消息回調函數
    private void OnClickAddButton()
    {
        //在這裏監聽按鈕的點擊事件
        LoginCtrl.Instance.StartGame();
    }

    //刪除Login外其他控件,例如
    public static void DestroyOtherUI()
    {
        Canvas canvas = GameMethod.GetCanvas;
        for (int i = 0; i < canvas.transform.childCount; i++)
        {
            if (canvas.transform.GetChild(i) != null && canvas.transform.GetChild(i).gameObject != null)
            {

                GameObject obj = canvas.transform.GetChild(i).gameObject;
                if (obj.name != "Login(Clone)")
                {
                    GameObject.DestroyImmediate(obj);
                }                    
            }
        }
    }

    //窗口控件釋放
    protected override void RealseWidget()
    {
    }

    //遊戲事件註冊
    protected override void OnAddListener()
    {

    }

    //遊戲事件注消
    protected override void OnRemoveListener()
    {

    }

    //顯示
    public override void OnEnable()
    {

    }

    //隱藏
    public override void OnDisable()
    {
    }
}

構造函數對資源文件和 UI 所在的場景類型初始化,以及該 UI 是否常住內存。後面函數是繼承 baseWindow 中具體實現,在函數中有 LoginCtrl 類接口調用,這個跟 LoginWidow 窗體息息相關,它是 MVC 中的 Control。下面再介紹 LoginCtrl 類的編寫。

控制類的主要作用是播放消息,然後在 Loginwindow 中觸發已設置監聽的函數,如 Show、顯示窗體,控制類的代碼如下所示:

        public class LoginCtrl : Singleton<LoginCtrl>
    {
        public void Enter()
        {
            EventCenter.Broadcast(EGameEvent.eGameEvent_LoginEnter);   
        }

        public void Exit()
        {
           EventCenter.Broadcast(EGameEvent.eGameEvent_LoginExit);
        }

        //登陸
        public void Login(string account, string pass)
        {

        }

        //登陸錯誤反饋
        public void LoginError(int code)
        {

        }


        //登陸失敗
        public void LoginFail()
        {

        }

        //開始遊戲
        public void StartGame()
        {
            SceneManager.LoadScene("Play");
            WindowManager.Instance.ChangeScenseToPlay(EScenesType.EST_Login);
            GameStateManager.Instance.ChangeGameStateTo(GameStateType.GST_Play);
        }

   }

我們將其設置成單例模式,在 Enter 函數中進行消息廣播,另外在函數 StartGame 中使用了 WindowManager 類接口和 GameStateManager 類接口。這兩個類很關鍵的,將在後面介紹。當然如果只有 Loginwindow 和 LoginCtrl 還是無法執行的,我們還缺少 State 狀態類,狀態類是負責窗體 UI 之間的切換,每個 UI 窗體對應着自己的狀態,爲了區分不同的 UI 窗體,利用這些狀態枚舉表示,代碼定義如下:

    public enum GameStateType
{
    GST_Continue,
    GST_Login,
    GST_Role,
    GST_Loading,
    GST_Play,
}

狀態類都包括那些方法?通常來說,我們會定義設置某個狀態、獲取狀態、進入狀態、停止狀態、更新狀態這些方法。我們在設計每個模塊時,都會先明確這個模塊具體要做哪些事情,其實在設計一個類內容時,自己可以多想想,這樣類的函數定義自然就有了。另外,提到的這些方法每個 UI 狀態都會包含,這樣我們將其抽離出來,定義成一個接口模塊供程序使用,代碼內容如下:

 public interface IGameState
{
    GameStateType GetStateType();
    void SetStateTo(GameStateType gsType);
    void Enter();
    GameStateType Update(float fDeltaTime);
    void FixedUpdate(float fixedDeltaTime);
    void Exit();
}

細心的讀者可能發現,一會抽象、一會接口,這裏涉及到一個抽象類和接口類的區別,爲了幫助讀者理解,在此介紹一下:抽象類實現了 oop 中的一個原則,把可變的與不可變的分離,所以 BaseWindow 採用了抽象的定義,UI 通用函數方法在父類中實現出來,而不通用的函數方法只提供接口,再介紹接口類,好的接口類定義應該是具有專一功能性的,而不是多功能的,否則造成接口污染。我們的狀態類 IGameState 功能是單一的,所以 IGameState 採用了接口的定義。窗體中的狀態類設計繼承 IGameState 類,代碼如下所示:

class LoginState : IGameState
{
   private  GameStateType _stateTo;
    //構造函數
    public LoginState()
    {
    }
    //獲取狀態
    public GameStateType GetStateType()
    {
        return GameStateType.GST_Login;
    }
    //設置狀態
    public void SetStateTo(GameStateType gs)
    {
        _stateTo = gs;
    }
    //進入狀態
    public void Enter()
    {
        SetStateTo(GameStateType.GST_Continue);

        LoginCtrl.Instance.Enter();        
    }
    //停止狀態
    public void Exit()
    {
        LoginCtrl.Instance.Exit();
    }

    public void FixedUpdate(float fixedDeltaTime)
    {

    }
    //更新狀態
    public GameStateType Update(float fDeltaTime)
    {
        return _stateTo;
    }

}

在這個 UI 窗體狀態類中實現了 IGameState 中定義的接口函數,UI 架構設計到這裏還是沒有完成,因爲我們還不知道咋用?在下章中給讀者介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章