第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);