游戲設計模式——Unity事件隊列(紀念京阿尼事件)


“對消息或事件的發送與受理進行時間上的解耦。”

 

在游戲開發過程中,經常會出現不同板塊之間的信息交流,或是存在“當...,就...”的情況,事件隊列編程模式可以有效解決消息傳遞中產生的腳本耦合問題,讓同一個板塊的腳本更加單純,不包含其他腳本的雜質內容,使腳本更容易最大程度的復用。

事件隊列模式的運行流程如下:

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中移除。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM