专栏首页U3D游戏设计模式——Unity事件队列(纪念京阿尼事件)

游戏设计模式——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中移除。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Unity TextMeshPro 一键生成工具

    https://blog.csdn.net/akof1314/article/details/80868869

    汐夜koshio
  • Unity 极简UI框架

    写ui的时候一般追求控制逻辑和显示逻辑分离,经典的类似于MVC,其余大多都是这个模式的衍生,实际上书写的时候M是在整个游戏的底层,我更倾向于将它称之为D(Dat...

    汐夜koshio
  • Unity 基于excel2json批处理读取Excel表并反序列化

    excel2json是一款将Excel表格文件快速生成json和C#数据类的高效插件,详情了解如下:

    汐夜koshio
  • Head First设计模式——复合模式

      复合模式是HeadFirst上面详细讲的最后一个模式,其前面的模式作者认为都是成熟的经常使用的模式。所以这是详细讲解模式的最后一篇,同时这个模式讲解的篇幅也...

    SpringSun
  • 温故而知新:设计模式之装饰模式(Decorator)

    小时候对日本的动画片十分着迷,“圣斗士”是我的最爱;长大后也曾经一度对“海贼王”十分痴迷;大学看武侠小说时,也特别喜欢那种主人公有奇遇的情况:吃到一颗千年异果,...

    菩提树下的杨过
  • 基于Redis实现的分布式锁

    Spring Cloud 分布式环境下,同一个服务都是部署在不同的机器上,这种情况无法像单体架构下数据一致性问题采用加锁就实现数据一致性问题,在高并发情况下,对...

    不会飞的小鸟
  • APK安装流程详解1——有关"安装ing"的实体类概述

    该类包含了从AndroidManifest.xml文件中收集的所有信息。 PackageInfo.java源码地址 通过源码我们知道PackageInfo是...

    隔壁老李头
  • Android RxJava 实战讲解:优雅实现 网络请求轮询

    注:关于 Rxjava中的延时创建操作符interval() 和 intervalRange()的使用请看文章Android RxJava:最基础的操作符详解 ...

    Carson.Ho
  • Decorator装饰者模式(结构型模式)

    假设让我们去设计FCL中的Stream类,该类具有流类的基本功能,除了有各种不同类型的流外(如内存流、文件流、网络流等等),但是在不同的业务场景下,如处理银行业...

    郑小超.
  • springboot整合druid连接池

    喜欢天文的pony站长

扫码关注云+社区

领取腾讯云代金券