“對消息或事件的發送與受理進行時間上的解耦。”
在遊戲開發過程中,經常會出現不同闆塊之間的資訊交流,或是存在“當...,就...”的情況,事件隊列程式設計模式可以有效解決消息傳遞中産生的腳本耦合問題,讓同一個闆塊的腳本更加單純,不包含其他腳本的雜質内容,使腳本更容易最大程度的複用。
事件隊列模式的運作流程如下:
1.當一個行為(Action)觸發了某一事件(Event)後,不是直接調用該事件,而是改為申請将其送出給廣播中心,也就是将自己的行為推入廣播材料的隊列末尾。
2.由中間的的廣播中心(事件隊列處理系統)統一管理播報這些行為,并且按隊列順利先後播報,播報完了沒有廣播材料(隊列為空)了就停下來摸魚。
3.關心這些行為的聽衆會向廣播中心注冊一個偵聽器(買個收音機聽廣播中心的播報),聽到自己感興趣的,就自發執行相應事件。
4.哪一天這個聽衆煩了就把收音機砸了,這樣偵聽器就被移除了,以後無論再發生什麼都跟自己沒關系。
是以,核心就是要建立這麼個廣播中心,這個廣播中心要能:
1.把稿子交過來(事件隊列入隊)
2.廣播材料,例如不好啦京阿尼被燒了,播完後把稿子扔了(觸發事件,事件隊列出隊)
3.檢視和管理收聽情況,誰誰誰在聽啥(申請注冊,移除)
知道這些之後,就可以來建造這麼一個廣播中心了,為了提升人氣,誰都可以來一下,這個廣播中心需要接受各式各樣的爆料,是以要用到泛型委托;
而且這個廣播中心是全世界獨一無二的,不能有好幾個執行個體,大家都要從我這過,是以要用到單例;
關于單例,可以看之前寫的部落格:
https://www.cnblogs.com/koshio0219/p/11203631.html
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4 using UnityEngine.Events;
5 using System;
6
7 public class GameEvent{ }
8
9 public class EventQueueSystem : MonoSingleton<EventQueueSystem>
10 {
11 public delegate void EventDelegate<T>(T e) where T : GameEvent;
12
13 private delegate void InternalEventDelegate(GameEvent e);
14
15 private Dictionary<Type, InternalEventDelegate> delegates = new Dictionary<Type, InternalEventDelegate>();
16 private Dictionary<Delegate, InternalEventDelegate> delegateLookup = new Dictionary<Delegate, InternalEventDelegate>();
17 private Dictionary<InternalEventDelegate, Delegate> delegateLookOnce = new Dictionary<InternalEventDelegate, Delegate>();
18
19 private Queue eventQueue = new Queue();
20
21 public bool bLimitQueueProcessing = false;
22 public float limitQueueTime = 0.1f;
23
24 //注冊偵聽事件(持續)
25 public static void AddListener<T>(EventDelegate<T> del) where T : GameEvent
26 {
27 Instance.AddDelegate(del);
28 }
29
30 //注冊偵聽事件(一次)
31 public static void AddListenerOnce<T>(EventDelegate<T> del) where T : GameEvent
32 {
33 var result = Instance.AddDelegate(del);
34 if (result != null)
35 Instance.delegateLookOnce[result] = del;
36 }
37
38 //判定偵聽事件是否存在
39 public static bool HasListener<T>(EventDelegate<T> del) where T : GameEvent
40 {
41 return Instance.delegateLookup.ContainsKey(del);
42 }
43
44 //移除偵聽事件
45 public static void RemoveListener<T>(EventDelegate<T> del) where T : GameEvent
46 {
47 if (Instance == null)
48 return;
49 if (Instance.delegateLookup.TryGetValue(del, out InternalEventDelegate eventDelegate))
50 {
51 if (Instance.delegates.TryGetValue(typeof(T), out InternalEventDelegate temp))
52 {
53 temp -= eventDelegate;
54 if (temp == null)
55 Instance.delegates.Remove(typeof(T));
56 else
57 Instance.delegates[typeof(T)] = temp;
58 }
59 Instance.delegateLookup.Remove(del);
60 }
61 }
62
63 public static void RemoveAll()
64 {
65 if (Instance != null)
66 {
67 Instance.delegates.Clear();
68 Instance.delegateLookup.Clear();
69 Instance.delegateLookOnce.Clear();
70 }
71 }
72
73 private InternalEventDelegate AddDelegate<T>(EventDelegate<T> del) where T : GameEvent
74 {
75 if (delegateLookup.ContainsKey(del))
76 return null;
77 void eventDelegate(GameEvent e) => del((T)e);
78 delegateLookup[del] = eventDelegate;
79
80 if (delegates.TryGetValue(typeof(T), out InternalEventDelegate temp))
81 delegates[typeof(T)] = temp += eventDelegate;
82 else
83 delegates[typeof(T)] = eventDelegate;
84 return eventDelegate;
85 }
86
87 //單個事件觸發
88 private static void TriggerEvent(GameEvent e)
89 {
90 var type = e.GetType();
91 if(Instance.delegates.TryGetValue(type,out InternalEventDelegate eventDelegate))
92 {
93 eventDelegate.Invoke(e);
94 //移除單一偵聽
95 foreach(InternalEventDelegate item in Instance.delegates[type].GetInvocationList())
96 {
97 if (Instance.delegateLookOnce.TryGetValue(item,out Delegate temp))
98 {
99 Instance.delegates[type] -= item;
100 if (Instance.delegates[type] == null)
101 Instance.delegates.Remove(type);
102 Instance.delegateLookup.Remove(temp);
103 Instance.delegateLookOnce.Remove(item);
104 }
105 }
106 }
107 }
108
109 //外部調用的推入事件隊列接口
110 public static void QueueEvent(GameEvent e)
111 {
112 if (!Instance.delegates.ContainsKey(e.GetType()))
113 return;
114 Instance.eventQueue.Enqueue(e);
115 }
116
117 //事件隊列觸發處理
118 void Update()
119 {
120 float timer = 0.0f;
121 while (eventQueue.Count > 0)
122 {
123 if (bLimitQueueProcessing)
124 if (timer > limitQueueTime)
125 return;
126 var e = eventQueue.Dequeue() as GameEvent;
127 TriggerEvent(e);
128 if (bLimitQueueProcessing)
129 timer += Time.deltaTime;
130 }
131 }
132
133 private void OnApplicationQuit()
134 {
135 RemoveAll();
136 eventQueue.Clear();
137 }
138 }
複制
下面是用法測試:
1.例如日本有一個京阿黑怨念深重,這一天終于爆發,他準備燒京阿尼啦,于是:
1 using UnityEngine;
2
3 public class 京阿黑 : MonoBehaviour
4 {
5 private void Start()
6 {
7 EventQueueSystem.QueueEvent(new 燒京阿尼計劃("我要燒京阿尼啦!已備好兩桶共計80升汽油!"));
8 //過了一段時間...
9 EventQueueSystem.QueueEvent(new 燒成功啦("成功啦!京阿尼被我燒死了20+啦!"));
10 }
11 }
複制
2.這是他準備爆料的稿子,之後廣播中心會按順利廣播這兩件事:
1 public class 燒京阿尼計劃 : GameEvent
2 {
3 public string plan;
4
5 public 燒京阿尼計劃(string plan)
6 {
7 this.plan = plan;
8 }
9 }
10
11 public class 燒成功啦 : GameEvent
12 {
13 public string result;
14
15 public 燒成功啦(string result)
16 {
17 this.result = result;
18 }
19 }
複制
3.國外有個京阿粉早就關注京阿尼的消息了,自然這兩件事他也不能放過,一開始他就注冊了偵聽,并且已經做好了應對措施:
1 using UnityEngine;
2 public class 京阿粉 : MonoBehaviour
3 {
4 private void Awake()
5 {
6 EventQueueSystem.AddListener<燒成功啦>(知道結果後);
7 EventQueueSystem.AddListener<燒京阿尼計劃>(聽了計劃後);
8 }
9
10 private void 知道結果後(燒成功啦 e)
11 {
12 Debug.Log(e.result);
13 Debug.Log("完了,ACG業界完了...");
14 }
15
16 private void OnDestroy()
17 {
18 EventQueueSystem.RemoveListener<燒京阿尼計劃>(聽了計劃後);
19 EventQueueSystem.RemoveListener<燒成功啦>(知道結果後);
20 }
21
22 private void 聽了計劃後(燒京阿尼計劃 e)
23 {
24 Debug.Log(e.plan);
25 Debug.Log("什麼?!我要去救京阿尼!");
26 }
27 }
複制
列印結果如下:

這裡有一點要注意,隻有在京阿粉早就關注了這兩個事件時才能在第一時間做出反應,也就是說,注冊偵聽器的時間需要比事件發出的時間早才行,不然就沒有效果。
2019年12月2日更新:
今天在使用上面的事件系統時發現了一個不太友善的地方,例如我想在類A腳本中添加對某一事件E的偵聽,但想在另一個腳本類B中去控制移除。
這時就有必要将事件E的委托函數記錄為一個全局變量,并且該委托函數可以在其他腳本中全局取得。這樣一想之後,就很容易得出解決方案了。
隻要将需要全局控制的委托變量統一放到一個單例類型的委托倉庫中就行了,可以對該倉庫中的委托進行指派或取值:
1 public class JoyStickUpEvent : GameEvent
2 {
3 public float TouchTime;
4 public JoyStickUpEvent(float touchTime)
5 {
6 TouchTime = touchTime;
7 }
8 }
複制
1 public class EventDelegate:Singleton<EventDelegate>
2 {
3 public EventQueueSystem.EventDelegate<JoyStickUpEvent> JoyStickUpHandler;
4 //...其他的全局委托
5 }
複制
類A中設定委托值并添加偵聽:
1 private void JoyStickUpHandler(JoyStickUpEvent e)
2 {
3 //處理搖杆搖桿擡起時的行為
4 }
複制
1 public override void OnAwake()
2 {
3 EventDelegate.Instance.JoyStickUpHandler = JoyStickUpHandler;
4 EventManager.AddListener(EventDelegate.Instance.JoyStickUpHandler);
5 }
複制
類B中控制移除偵聽:
1 public override void Dead()
2 {
3 EventManager.RemoveListener(EventDelegate.Instance.JoyStickUpHandler);
4 }
複制
這樣一來,無論是事件的觸發還是委托的全局修改都将變得更為靈活和容易,甚至可以在類A中對委托指派,在類B中添加對應的事件偵聽,在類C中移除。