[Unity] 獨立自控的 UI 切換架構 – Independent UI Switching

過去在我的其他文章介紹過利用 狀態模式 / 狀態機 來控制遊戲程式的流程,是我相當喜歡且穩定的作法。在一般使用狀態機的情境下,所有的 UI 開關都由狀態機中的邏輯決定,UI 本身只是作為畫面表現的客體存在。

不過若用狀態機做為開發的專案核心,則代表會需要有一套是先架構的核心系統。如果是臨時的小型 prototype,或者在 game jam 環境展開的專案,引入一套架構並要新成員馬上建立起使用規範來利用這套系統,是有點不實際的。

所以是否有擺脫狀態機,甚至沒有一個集中式核心的小型替代方案可以用來管理流程跟 UI 切換呢?本文便是以此為出發點,所做的一個嘗試。

架構簡介

傳統網頁的伺服器,主要依賴網址來獲取顯示的要求,依照要求回傳網頁給予使用者。這個溝通模式可以被歸類為 Request-Response Model,而這也是我這次新架構的主要想法來源,雖然最後結果上不能說是同一套 Model。

在我的架構之中,每一頁 UI page 同時也代表著當下遊戲進行的階段,當遊戲到某階段時的邏輯處理,都跟 UI page 的啟動同時發生,就像在瀏覽器上切換畫面一樣。這代表遊戲的進程由 UI page 推動著,而非利用狀態機與中央管理畫面的類別來推動。

每個 UI page 基本上獨立而不依賴於中央管理器,各自接受了啟動需求、按鈕等事件,做出對應的反應:開啟、關閉或者其他呈現上的變化,而非開放各項操作方法來給中央管理器進行統一操作。

** 這篇文章的用詞會用 UI page 來代表每個介面單位,但實際使用上不只是 UI,只要是場景上的呈現都可以透過這方法呼叫,所以類別用 View 來命名指稱被呼叫的呈現畫面。

ViewBase

using UnityEngine;

public abstract class ViewBase : MonoBehaviour {

    public event System.Action<ViewId> onViewRequest;

    public virtual void Initialize () {
        this.gameObject.SetActive (false);
    }

    public virtual void GetRequest () {
        this.gameObject.SetActive (true);
    }

    protected void SendRequest (ViewId viewId) {
        onViewRequest (viewId);
    }
}

ViewBase 繼承了 MonoBehaviour,同時也是個 abstract class,每個 UI page 都應該有一份繼承後的類別,並掛載於該 UI 的根物件之上。這裡跟狀態機的 State 類別有點相似,只是改成以 場景上的物件作為本體。

ViewBase 的成員有:

  • onViewRequest 成員,一個帶有 event 關鍵字的 delegate,用於將需要新頁面的要求送出。
  • SendRequest (ViewId) 方法,實際用於呼叫新頁面的方法,是對應於 event 的必要封裝。
  • Initialize () 方法,會在初始化被呼叫一次,預設的動作是將 UI 物件整個 SetActive (false) 進入待命狀態,也可 override 成需要的初始化 (如:做螢幕適屏的處理)。

其中最重要的:

  • GetRequest () 方法,用於接收從其他 UI page 傳來的要求,代表需要啟動此頁 UI page。預設動作是將物件 SetActive (true),但是如果有進場動畫或者其他需求,可以 override 這個方法。

類別之中並沒有實現關於關閉 UI 或畫面呈現的方法,因為這點在設計上由各畫面自行決定,這邊用一個例子進行說明:

public void ClickStart () {
    SendRequest (ViewId.BattleInfo);
    this.gameObject.SetActive (false);
}

這是一個會 Invoke 在 uGUI Button 上的方法,代表 Menu 頁面的 Start 按鈕被按下了,因而觸發開啟 BattleInfo 頁面的需求,並將自身頁面關閉。這便是此架構中,讓畫面與 UI 事件來推動遊戲進程,開啟與關閉相關物件的最簡單例子。

除此之外,因為遊戲邏輯而產生需要切換畫面的需求,也用類似的方法處理。

ViewId

[System.Flags]
public enum ViewId {
    Menu = 1,
    BattleInfo = 2,
    Result = 4
}

ViewId 在這裡的地位就類似瀏覽器上輸入的網址,用於描述需要頁面的資訊。這裡是使用 enum 作為實現方式,如果要用 string 或 int 等其他方式取代也無不可。

此處使用 enum 時,我會特別加上 System.Flags,可以讓 enum 使用位元運算子來疊加多個需求,而不必用上陣列或者 params object[] 之類的方法。

ViewRouting

using UnityEngine;

public class ViewRoutingTest : MonoBehaviour {

    [SerializeField]
    private ViewBase[] m_viewBases;

    private void Awake () {
		for (int i = 0; i < m_viewBases.Length; i++) {
            m_viewBases[i].Initialize ();
            m_viewBases[i].onViewRequest += ViewRouting;
        }
	}

    private void Start () {
        ViewRouting (ViewId.Menu);
    }

    private void ViewRouting (ViewId viewId) {
        if ((viewId & ViewId.Menu) == ViewId.Menu) {
            m_viewBases[0].GetRequest ();
        }
        if ((viewId & ViewId.BattleInfo) == ViewId.BattleInfo) {
            m_viewBases[1].GetRequest ();
        }
        if ((viewId & ViewId.Result) == ViewId.Result) {
            m_viewBases[2].GetRequest ();
        }
    }
}

前面有提到,這個架構沒有中央處理器跟核心。不過依舊需要一個初始化的腳本,用於處理每個 UI  page 丟出來的 request,交給對應的 ViewBase 處理。

這裡的 ViewRoutingTest 便是一個例子,在 Awake 呼叫了所有 ViewBase 的 Initialize () 來進行初始化,並將一個 ViewRouting (ViewId) 的方法註冊到所有的 onViewRequest event 中,用來接收 request。

在 ViewRouting (ViewId) 方法中,會將接收到的參數經過一連串邏輯,並對相關的 ViewBase 進行呼叫,如果有跟畫面切換同時發生的遊戲邏輯,也可以在這裡進行處理。這個方法就類似於一些網站框架的 Route 處理器。

這裡的例子因為對應 ViewId 是一個有 Flags 屬性的 enum,所以對應地要利用位元運算跟 if 來處理疊加的需求,而非用 switch 來處理。

初始化完畢後,在 Start 中呼叫第一個啟動的頁面,就代表遊戲正式啟動了,接下來的流程推動將由各個獨立的 UI  page 來進行。

** 初始化 ViewBase 時要注意不同物件之間的初始化順序,避免有些物件還沒準備好就開始 Request 第一個頁面。

小結

這邊列出我目前使用這個架構感受到的幾個優缺點,可以看出來還有不少細節需要考慮改善,但因為整體相當輕量化,未來在 game jam 等快速開發的情境下我應該會常常用上。

如果有更加完善化的版本出現,我會在撰文跟大家分享。

優點

  • 架構所需的程式碼較少。
  • 沒有核心管理器的存在,只有一份初始化的腳本,使用規範較寬鬆。
  • 跟 Unity 的 uGUI 及各種事件的互通性良好,基本上不需要調整太多開發方式。
  • UI 的換場、進退場等需要時間撥放的動畫,可以由 UI 自行管理,也不用擔心跟狀態機的切換邏輯要如何配合。

缺點

  • 遊戲進程跟 UI 流程相互依賴,所以流程彈性跟 UI 的複用性較差。
  • 沒有核心管理器的同時,也代表橫跨整個遊戲的大型系統較不易建立。
  • 跟場景切換的需求較不好配合。
  • 對於場景初始化順序,因為沒有核心進行管理,相當考驗對物件 Awake、Start 的規劃。

[Unity] 獨立自控的 UI 切換架構 – Independent UI Switching 有 “ 4 則迴響 ”

  1. 看完後想問幾個地方
    1.
    中間有一段 ClickStart 方法的實現,是不是各頁面自行繼承ViewBase ,再擴充各自需求。
    〝類別之中並沒有實現關於關閉 UI 或畫面呈現的方法,因為這點在設計上由各畫面自行決定,這邊用一個例子進行說明:〞

    2.
    ViewRoutingTest 中的 ViewRouting 方法
    Code中將if判斷後
    分別分配如以下
    m_viewBases[0] = ViewId.Menu
    m_viewBases[1] = ViewId.BattleInfo
    m_viewBases[2] = ViewId.Result
    那是否代表 這些在從 Unity編輯器中拖曳給m_viewBases 陣列的時候就得跟Enum內容綁死了呢

    不知道是否有理解錯誤
    以下是我想到的改進方式,不知道跟你原本的想法是不是一樣。
    在ViewBase中新增protected ViewId viewID ;
    在 ViewBase 的延伸類別中,假設是Menu的頁面
    public override void Initialize () {
    base.Initialize();
    viewID = ViewId.Menu;
    }
    這樣在ViewRoutingTest 的 ViewRouting 方法就可以透過viewID 在陣列中尋找,
    並呼叫對應的類別的GetRequest。
    進而節省每次新增頁面時都要在ViewRouting 實現該頁面的判斷。

    1. 用 List.Find() 的做法也沒問題,routing 的部分依照需求去調整即可。

      這兩者沒有確切的好壞,假如使用 List.Find(),就不能使用 Flags 的疊加 enum 了,同時要在呼叫 View 的同時加入特定邏輯也比較麻煩。

      比如,在切換到 BattleInfo 的同時,也啟動玩家腳色的控制等。

    1. 是,我文章也提到可以依照需求傳遞不同形式的參數,不一定要使用 enum。如果把資料傳遞進 GetRequest,就可以同時進行一些變化了。

發表留言