unity 網絡遊戲架構設計(第02課:自定義消息分發類模塊)之美

第02課:自定義消息分發類模塊

爲什麼要使用消息分發函數?在 Unity 代碼設計中,這個問題是不可迴避的,因爲在開發產品時,不可避免的是各個模塊之間會有或多或少的聯繫,但是爲了模塊的擴展性,各個代碼模塊之間的耦合性必須降低,否則產品上線後,版本迭代會出現各種問題。有人可能會說,可以使用單例模式、靜態類等等,在此就給讀者普及一下知識點。

先說一下單例模式,如果邏輯相對來說比較簡單,它是可以的,但是如果邏輯比較複雜,那單例的調用會非常頻繁,從而導致邏輯混亂,這是不可取的。靜態類是常駐內存的,在遊戲開發中除了一些指定的加載數據常駐內存,一般不會使用過多的靜態類,所以也是不可取的。而且單例和靜態二者也不會降低模塊之間的耦合性,最終我們只能考慮消息分發函數,下面先介紹 Unity 引擎自帶的消息分發函數。

Unity 自帶的消息分發函數

Unity 引擎也爲開發者提供了消息分發函數:SendMessage、SendMessageUpwards、BroadcastMessage,它們也可以實現簡單的消息發送,函數內部的參數在這裏就不一一介紹了。現在說一下爲什麼不選擇它,因爲它們的執行效率相對委託來說是比較低的,網上有關於測試效率的案例,而且擴展性方面也不好,比如我會使用很多的參數進行傳遞,它很難滿足我們的需求,遊戲開發還會有更多的類似需求。所以我們放棄它們,選擇使用委託自己去封裝。

爲什麼自定義消息分發類模塊

自己定義消息分發,選擇的也是委託的方式,首先我們要清楚封裝事件是用於做啥事情的?先舉一個需求說明。

當玩家殺怪獲取到掉落下來的道具時,玩家的經驗值加1。這是一個很基礎的功能需求,這類需求充斥着遊戲的所有地方。當然我們可以不使用事件系統,直接在 OnTriggerEnter 方法中給該玩家的生命值加1就好了,但是,這將使得檢測碰撞的這塊代碼直接引用了玩家屬性管理的代碼,也就是代碼的緊耦合。而且,在後來的某一天,我們又想讓接到道具的同時還在界面上顯示一個圖標,這時又需要在這裏引用界面相關的代碼。後來,又希望能播放一段音效……,這樣隨着需求的增加,邏輯會越來越複雜。解決此問題的好辦法就是,在 OnTrigerEnter 中加入消息分發函數,這樣具體的操作就在另一個類的函數中進行,耦合性降低。

另外,在網絡遊戲中,我們也會遇到服務器發送給客戶端角色信息後,客戶端接收到該消息後,接下來做會將得到的角色信息在 UI 上顯示出來。如果不用事件系統對其進行分離,那麼網絡消息跟 UI 就混在一起了。這樣隨着邏輯的需求增加,耦合性會越來越大,最後會導致項目很難維護。

既然事件系統這麼重要,我們必須要使用它解耦合模塊,下面說說設計思路。

事件系統的設計思想

遊戲中會有很多事件,事件的分類表示我們可以採用字符串或者採用枚舉值,事件系統使用的是枚舉值,事件分類枚舉代碼表示如下所示:

    public enum EGameEvent
{
    eWeaponDataChange = 1,
    ePlayerShoot = 2,

    //UI
    eLevelChange = 3,
    eBloodChange = 4,
    ePowerChange = 5,
    eSkillInit = 6,
    eSkillUpdate = 7,
    eBuffPick = 8,
    eTalent = 9,


    eBlood = 10,
    eMp = 11,
    eScore = 12,
    ePower = 13,
    eTalentUpdate = 14,
    ePickBuff = 15,

    eGameEvent_LockTarget,

    //Login
    eGameEvent_LoginSuccess,  //登陸成功
    eGameEvent_LoginEnter, //登錄界面
    eGameEvent_LoginExit,
    eGameEvent_RoleEnter,
    eGameEvent_RoleExit,
    //Play
    eGameEvent_PlayEnter,
    eGameEvent_PlayExit,

    ePlayerInput,

    eActorDead,
}

這些事件分類還可以繼續擴展,事件系統貫穿於整個遊戲,從 UI 界面、登錄、戰鬥等等。我們的事件系統實現主要分爲三步:事件監聽、事件分發、事件移除。還有一個問題,事件和委託是保存在哪裏的?我們使用了字典 Dictionary 用於保存事件和委託。代碼如下:

    static public Dictionary<EGameEvent, Delegate> mEventTable = new Dictionary<EGameEvent, Delegate>(); 

事件系統中的委託,也需要我們自己封裝,可以思考一下,委託該如何封裝?我們使用的委託函數的參數可能會有多個,而且不同的委託函數對應的類型可能也是不同的,比如 GameObject、float、int 等等。針對這些需求,唯一能幫我們解決問題的就是模版類,回調函數對應的代碼如下:

public delegate void Callback();
public delegate void Callback<T>(T arg1);
public delegate void Callback<T, U>(T arg1, U arg2);
public delegate void Callback<T, U, V>(T arg1, U arg2, V arg3);
public delegate void Callback<T, U, V, X>(T arg1, U arg2, V arg3, X arg4);

最多列舉了四個參數的回調函數,下面開始事件類的封裝了。先封裝監聽函數:

    //無參數
    static public void AddListener(EGameEvent eventType, Callback handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback)mEventTable[eventType] + handler;
    }

    //一個參數
    static public void AddListener<T>(EGameEvent eventType, Callback<T> handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback<T>)mEventTable[eventType] + handler;
    }

    //兩個參數
    static public void AddListener<T, U>(EGameEvent eventType, Callback<T, U> handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback<T, U>)mEventTable[eventType] + handler;
    }

    //三個參數
    static public void AddListener<T, U, V>(EGameEvent eventType, Callback<T, U, V> handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback<T, U, V>)mEventTable[eventType] + handler;
    }

    //四個參數
    static public void AddListener<T, U, V, X>(EGameEvent eventType, Callback<T, U, V, X> handler) {
        OnListenerAdding(eventType, handler);
        mEventTable[eventType] = (Callback<T, U, V, X>)mEventTable[eventType] + handler;
    }

每個函數都比較簡單,從沒有參數,到最多四個參數的函數一一給讀者展示出來。這些函數都調用了函數 OnListenerAdding 用於將事件和委託粗放到字典中,監聽函數有了,對應的就是移除監聽函數,移除就是從 Dictionary 字典中將其移除掉,它跟監聽函數是一一對應的函數如下:

    //No parameters
    static public void RemoveListener(EGameEvent eventType, Callback handler) {
        OnListenerRemoving(eventType, handler);   
        mEventTable[eventType] = (Callback)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //Single parameter
    static public void RemoveListener<T>(EGameEvent eventType, Callback<T> handler) {
        OnListenerRemoving(eventType, handler);
        mEventTable[eventType] = (Callback<T>)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //Two parameters
    static public void RemoveListener<T, U>(EGameEvent eventType, Callback<T, U> handler) {
        OnListenerRemoving(eventType, handler);
        mEventTable[eventType] = (Callback<T, U>)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //Three parameters
    static public void RemoveListener<T, U, V>(EGameEvent eventType, Callback<T, U, V> handler) {
        OnListenerRemoving(eventType, handler);
        mEventTable[eventType] = (Callback<T, U, V>)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //Four parameters
    static public void RemoveListener<T, U, V, X>(EGameEvent eventType, Callback<T, U, V, X> handler) {
        OnListenerRemoving(eventType, handler);
        mEventTable[eventType] = (Callback<T, U, V, X>)mEventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }    

監聽函數和移除監聽函數都封裝完了,那麼如何觸發監聽函數這就是我們通常所說的廣播函數,它與監聽和移除也是一一對應的,代碼片段如下所示:

    //No parameters
    static public void Broadcast(EGameEvent eventType) {
        OnBroadcasting(eventType);
        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback callback = d as Callback;

            if (callback != null) {
                callback();
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //Single parameter
    static public void Broadcast<T>(EGameEvent eventType, T arg1) {

        OnBroadcasting(eventType);

        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback<T> callback = d as Callback<T>;

            if (callback != null) {
                callback(arg1);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //Two parameters
    static public void Broadcast<T, U>(EGameEvent eventType, T arg1, U arg2) {

        OnBroadcasting(eventType);

        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback<T, U> callback = d as Callback<T, U>;

            if (callback != null) {
                callback(arg1, arg2);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //Three parameters
    static public void Broadcast<T, U, V>(EGameEvent eventType, T arg1, U arg2, V arg3) {

        OnBroadcasting(eventType);

        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback<T, U, V> callback = d as Callback<T, U, V>;

            if (callback != null) {
                callback(arg1, arg2, arg3);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //Four parameters
    static public void Broadcast<T, U, V, X>(EGameEvent eventType, T arg1, U arg2, V arg3, X arg4) {

        OnBroadcasting(eventType);

        Delegate d;
        if (mEventTable.TryGetValue(eventType, out d)) {
            Callback<T, U, V, X> callback = d as Callback<T, U, V, X>;

            if (callback != null) {
                callback(arg1, arg2, arg3, arg4);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }    
}

另外把 OnListenerAdding 函數封裝如下,它主要是將事件和委託存放到字典中,如下所示:

    static public void OnListenerAdding(EGameEvent eventType, Delegate listenerBeingAdded) {
    if (!mEventTable.ContainsKey(eventType)) {
        mEventTable.Add(eventType, null );
    }

    Delegate d = mEventTable[eventType];
    if (d != null && d.GetType() != listenerBeingAdded.GetType()) {
        throw new ListenerException(string.Format("Attempting to add listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being added has type {2}", eventType, d.GetType().Name, listenerBeingAdded.GetType().Name));
    }
}

這樣我們的整個事件系統就封裝完成了,最後告訴讀者如何使用?首先需要先監聽,將監聽函數放在對應的類中,代碼如下所示:

EventCenter.AddListener(EGameEvent.eGameEvent_GamePlayEnter, Show);

然後在另一個類文件中,可以播放此消息。代碼如下所示:

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