游戏设计模式——Unity事件队列(纪念京阿尼事件)

2020-03-19 15:52:41 浏览数 (1)

“对消息或事件的发送与受理进行时间上的解耦。”

在游戏开发过程中,经常会出现不同板块之间的信息交流,或是存在“当...,就...”的情况,事件队列编程模式可以有效解决消息传递中产生的脚本耦合问题,让同一个板块的脚本更加单纯,不包含其他脚本的杂质内容,使脚本更容易最大程度的复用。

事件队列模式的运行流程如下:

1.当一个行为(Action)触发了某一事件(Event)后,不是直接调用该事件,而是改为申请将其提交给广播中心,也就是将自己的行为推入广播材料的队列末尾。

2.由中间的的广播中心(事件队列处理系统)统一管理播报这些行为,并且按队列顺利先后播报,播报完了没有广播材料(队列为空)了就停下来摸鱼。

3.关心这些行为的听众会向广播中心注册一个侦听器(买个收音机听广播中心的播报),听到自己感兴趣的,就自发执行相应事件。

4.哪一天这个听众烦了就把收音机砸了,这样侦听器就被移除了,以后无论再发生什么都跟自己没关系。

所以,核心就是要建立这么个广播中心,这个广播中心要能:

1.把稿子交过来(事件队列入队)

2.广播材料,例如不好啦京阿尼被烧了,播完后把稿子扔了(触发事件,事件队列出队)

3.查看和管理收听情况,谁谁谁在听啥(申请注册,移除)

知道这些之后,就可以来建造这么一个广播中心了,为了提升人气,谁都可以来一下,这个广播中心需要接受各式各样的爆料,所以要用到泛型委托;

而且这个广播中心是全世界独一无二的,不能有好几个实例,大家都要从我这过,所以要用到单例;

关于单例,可以看之前写的博客:

https://cloud.tencent.com/developer/article/1601263

代码语言:javascript复制
  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.例如日本有一个京阿黑怨念深重,这一天终于爆发,他准备烧京阿尼啦,于是:

代码语言:javascript复制
 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.这是他准备爆料的稿子,之后广播中心会按顺利广播这两件事:

代码语言:javascript复制
 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.国外有个京阿粉早就关注京阿尼的消息了,自然这两件事他也不能放过,一开始他就注册了侦听,并且已经做好了应对措施:

代码语言:javascript复制
 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的委托函数记录为一个全局变量,并且该委托函数可以在其他脚本中全局取得。这样一想之后,就很容易得出解决方案了。

只要将需要全局控制的委托变量统一放到一个单例类型的委托仓库中就行了,可以对该仓库中的委托进行赋值或取值:

代码语言:javascript复制
1 public class JoyStickUpEvent : GameEvent
2 {
3     public float TouchTime;
4     public JoyStickUpEvent(float touchTime)
5     {
6         TouchTime = touchTime;
7     }
8 }
代码语言:javascript复制
1 public class EventDelegate:Singleton<EventDelegate>
2 {
3     public EventQueueSystem.EventDelegate<JoyStickUpEvent> JoyStickUpHandler;
4     //...其他的全局委托 
5 }

类A中设置委托值并添加侦听:

代码语言:javascript复制
1     private void JoyStickUpHandler(JoyStickUpEvent e)
2     {
3         //处理摇杆手柄抬起时的行为
4     }
代码语言:javascript复制
1     public override void OnAwake()
2     {
3         EventDelegate.Instance.JoyStickUpHandler = JoyStickUpHandler;
4         EventManager.AddListener(EventDelegate.Instance.JoyStickUpHandler);
5     }

类B中控制移除侦听:

代码语言:javascript复制
1     public override void Dead()
2     {
3         EventManager.RemoveListener(EventDelegate.Instance.JoyStickUpHandler);
4     }

这样一来,无论是事件的触发还是委托的全局修改都将变得更为灵活和容易,甚至可以在类A中对委托赋值,在类B中添加对应的事件侦听,在类C中移除。

0 人点赞