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

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

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

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

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

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

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

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

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

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

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

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

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

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

https://www.cnblogs.com/koshio0219/p/11203631.html

 using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System; public class GameEvent{ } public class EventQueueSystem : MonoSingleton<EventQueueSystem>
{
public delegate void EventDelegate<T>(T e) where T : GameEvent; private delegate void InternalEventDelegate(GameEvent e); private Dictionary<Type, InternalEventDelegate> delegates = new Dictionary<Type, InternalEventDelegate>();
private Dictionary<Delegate, InternalEventDelegate> delegateLookup = new Dictionary<Delegate, InternalEventDelegate>();
private Dictionary<InternalEventDelegate, Delegate> delegateLookOnce = new Dictionary<InternalEventDelegate, Delegate>(); private Queue eventQueue = new Queue(); public bool bLimitQueueProcessing = false;
public float limitQueueTime = 0.1f; //注册侦听事件(持续)
public static void AddListener<T>(EventDelegate<T> del) where T : GameEvent
{
Instance.AddDelegate(del);
} //注册侦听事件(一次)
public static void AddListenerOnce<T>(EventDelegate<T> del) where T : GameEvent
{
var result = Instance.AddDelegate(del);
if (result != null)
Instance.delegateLookOnce[result] = del;
} //判定侦听事件是否存在
public static bool HasListener<T>(EventDelegate<T> del) where T : GameEvent
{
return Instance.delegateLookup.ContainsKey(del);
} //移除侦听事件
public static void RemoveListener<T>(EventDelegate<T> del) where T : GameEvent
{
if (Instance == null)
return;
if (Instance.delegateLookup.TryGetValue(del, out InternalEventDelegate eventDelegate))
{
if (Instance.delegates.TryGetValue(typeof(T), out InternalEventDelegate temp))
{
temp -= eventDelegate;
if (temp == null)
Instance.delegates.Remove(typeof(T));
else
Instance.delegates[typeof(T)] = temp;
}
Instance.delegateLookup.Remove(del);
}
} public static void RemoveAll()
{
if (Instance != null)
{
Instance.delegates.Clear();
Instance.delegateLookup.Clear();
Instance.delegateLookOnce.Clear();
}
} private InternalEventDelegate AddDelegate<T>(EventDelegate<T> del) where T : GameEvent
{
if (delegateLookup.ContainsKey(del))
return null;
void eventDelegate(GameEvent e) => del((T)e);
delegateLookup[del] = eventDelegate; if (delegates.TryGetValue(typeof(T), out InternalEventDelegate temp))
delegates[typeof(T)] = temp += eventDelegate;
else
delegates[typeof(T)] = eventDelegate;
return eventDelegate;
} //单个事件触发
private static void TriggerEvent(GameEvent e)
{
var type = e.GetType();
if(Instance.delegates.TryGetValue(type,out InternalEventDelegate eventDelegate))
{
eventDelegate.Invoke(e);
//移除单一侦听
foreach(InternalEventDelegate item in Instance.delegates[type].GetInvocationList())
{
if (Instance.delegateLookOnce.TryGetValue(item,out Delegate temp))
{
Instance.delegates[type] -= item;
if (Instance.delegates[type] == null)
Instance.delegates.Remove(type);
Instance.delegateLookup.Remove(temp);
Instance.delegateLookOnce.Remove(item);
}
}
}
} //外部调用的推入事件队列接口
public static void QueueEvent(GameEvent e)
{
if (!Instance.delegates.ContainsKey(e.GetType()))
return;
Instance.eventQueue.Enqueue(e);
} //事件队列触发处理
void Update()
{
float timer = 0.0f;
while (eventQueue.Count > )
{
if (bLimitQueueProcessing)
if (timer > limitQueueTime)
return;
var e = eventQueue.Dequeue() as GameEvent;
TriggerEvent(e);
if (bLimitQueueProcessing)
timer += Time.deltaTime;
}
} private void OnApplicationQuit()
{
RemoveAll();
eventQueue.Clear();
}
}

下面是用法测试:

1.例如日本有一个京阿黑怨念深重,这一天终于爆发,他准备烧京阿尼啦,于是:

 using UnityEngine;

 public class 京阿黑 : MonoBehaviour
{
private void Start()
{
EventQueueSystem.QueueEvent(new 烧京阿尼计划("我要烧京阿尼啦!已备好两桶共计80升汽油!"));
//过了一段时间...
EventQueueSystem.QueueEvent(new 烧成功啦("成功啦!京阿尼被我烧死了20+啦!"));
}
}

2.这是他准备爆料的稿子,之后广播中心会按顺利广播这两件事:

 public class 烧京阿尼计划 : GameEvent
{
public string plan; public 烧京阿尼计划(string plan)
{
this.plan = plan;
}
} public class 烧成功啦 : GameEvent
{
public string result; public 烧成功啦(string result)
{
this.result = result;
}
}

3.国外有个京阿粉早就关注京阿尼的消息了,自然这两件事他也不能放过,一开始他就注册了侦听,并且已经做好了应对措施:

 using UnityEngine;
public class 京阿粉 : MonoBehaviour
{
private void Awake()
{
EventQueueSystem.AddListener<烧成功啦>(知道结果后);
EventQueueSystem.AddListener<烧京阿尼计划>(听了计划后);
} private void 知道结果后(烧成功啦 e)
{
Debug.Log(e.result);
Debug.Log("完了,ACG业界完了...");
} private void OnDestroy()
{
EventQueueSystem.RemoveListener<烧京阿尼计划>(听了计划后);
EventQueueSystem.RemoveListener<烧成功啦>(知道结果后);
} private void 听了计划后(烧京阿尼计划 e)
{
Debug.Log(e.plan);
Debug.Log("什么?!我要去救京阿尼!");
}
}

打印结果如下:

这里有一点要注意,只有在京阿粉早就关注了这两个事件时才能在第一时间做出反应,也就是说,注册侦听器的时间需要比事件发出的时间早才行,不然就没有效果。

2019年12月2日更新:

今天在使用上面的事件系统时发现了一个不太方便的地方,例如我想在类A脚本中添加对某一事件E的侦听,但想在另一个脚本类B中去控制移除。

这时就有必要将事件E的委托函数记录为一个全局变量,并且该委托函数可以在其他脚本中全局取得。这样一想之后,就很容易得出解决方案了。

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

 public class JoyStickUpEvent : GameEvent
{
public float TouchTime;
public JoyStickUpEvent(float touchTime)
{
TouchTime = touchTime;
}
}
 public class EventDelegate:Singleton<EventDelegate>
{
public EventQueueSystem.EventDelegate<JoyStickUpEvent> JoyStickUpHandler;
//...其他的全局委托
}

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

     private void JoyStickUpHandler(JoyStickUpEvent e)
{
//处理摇杆手柄抬起时的行为
}
     public override void OnAwake()
{
EventDelegate.Instance.JoyStickUpHandler = JoyStickUpHandler;
EventManager.AddListener(EventDelegate.Instance.JoyStickUpHandler);
}

类B中控制移除侦听:

     public override void Dead()
{
EventManager.RemoveListener(EventDelegate.Instance.JoyStickUpHandler);
}

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

游戏设计模式——Unity事件队列(纪念京阿尼事件)的更多相关文章

  1. 游戏设计模式——Unity对象池

    对象池这个名字听起来很玄乎,其实就是将一系列需要反复创建和销毁的对象存储在一个看不到的地方,下次用同样的东西时往这里取,类似于一个存放备用物质的仓库. 它的好处就是避免了反复实例化个体的运算,能减少大 ...

  2. 游戏设计模式——C++单例类

    前言: 本文将探讨单例类设计模式,单例类的懒汉模式/饿汉模式,单例类的多线程安全性,最后将利用C++模板减少单例类代码量. 本文假设有一个Manager管理类,并以此为探究单例类的设计模式. 懒汉模式 ...

  3. Qt使用一个事件队列对所有发出的事件进行维护(QObject的event()函数相当于dispatch函数),用EventLabel 继承QLabel作为例子(简单明了) good

    事件(event)是由系统或者 Qt 本身在不同的时刻发出的.当用户按下鼠标.敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件.一些事件在对用户操作做出响应时发出,如键盘事件等:另一些事 ...

  4. 游戏设计模式系列(一)—— 单线逻辑&&数据驱动,搞定最容易卡死的结算界面

    从事游戏行业1年多了,个中心酸不知从何说起.抛开非技术的不说,一个开发者需要面对的最大问题,可能就是和策划频繁改变的需求做斗争了吧,这时候就体现了设计模式的重要性,抛开正式的设计方式不说,先讲讲我1年 ...

  5. 独立开发游戏越来越容易:Unity 发布旗下的最新游戏引擎 Unity 5,依然有免费版(转)

    独立开发者开发游戏正变得越来越容易,因为在游戏设计中很多吃力不讨好的工作可以直接采用像 Epic Games 或 Unity Technologies 这样的游戏引擎来解决.而这几天,游戏引擎商们先后 ...

  6. 游戏设计模式:Subclass Sandbox模式,以及功能方法集的设计思考

    书中总结出这种 Subclass Sandbox 的设计模式 Game Design Patterns: Subclass Sandbox 这种模式要点有两点: 在基类中实现各种功能性方法供子类调用 ...

  7. 游戏开发设计模式之状态模式 & 有限状态机 & c#委托事件(unity3d 示例实现)

    命令模式:游戏开发设计模式之命令模式(unity3d 示例实现) 对象池模式:游戏开发设计模式之对象池模式(unity3d 示例实现) 原型模式:游戏开发设计模式之原型模式 & unity3d ...

  8. 【游戏开发&Unity】捏脸系统(附源码)

    本着“没有捏脸系统算什么RPG”的想法,着手做一个2d简易捏脸demo.其实换装游戏都差不多啦~ github代码地址:Simple-Character-Edit-System (Unity版本:5. ...

  9. 怎样将游戏从Unity导到iOS设备上

    当我开始开发自己的iOS游戏时,我会考虑的第一件事便是如何将其导出到设备中,如此有效地测试我的游戏.最初,该过程看似很长且复杂,我所遇到的主要问题是,尽管存在许多资源,但是它们并非完全来自同样的地方, ...

随机推荐

  1. LeetCode--回文数(简单)

    题目描述: 判断一个整数是否是回文数.回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数. 示例 1: 输入: 121 输出: true 示例 2: 输入: -121 输出: false 解 ...

  2. Django中的sql注入

    Django中防止SQL注入的方法 方案一总是使用Django自带的数据库API.它会根据你所使用的数据库服务器(例如PostSQL或者MySQL)的转换规则,自动转义特殊的SQL参数.这被运用到了整 ...

  3. 测试:Oracle 19c RAC添加私网

    最近有个客户需求是在某12.2版本的RAC环境上添加心跳网络,顺便考虑将之前的心跳网络改为asm专用.我目前只有19c的RAC的测试环境(19c是12c的最终稳定版本),直接测试验证下过程备忘. 1. ...

  4. ASP.NET Core 2.2 WebApi 系列【五】MiniProfiler与Swagger集成

    MiniProfiler 是一款性能分析的轻量级程序,可以基于action(request)记录每个阶段的耗时时长,还是可以显示访问数据库时的SQL(支持EF.EF Code First)等 一.安装 ...

  5. navicat 12激活

    激活软件:https://github.com/DoubleLabyrinth/navicat-keygen/releases 激活说明:https://github.com/DoubleLabyri ...

  6. Pycharm自带Git实现版本管理

    之前一直使用本地的git客户端,通过命令来上传.下载代码到Gitlab:每次都需要启动git客户端,敲git命令来完成,不够灵活,因为强大的Pycharm就自带git功能,可以直接在Pycharm完成 ...

  7. ES6变量的解构赋值(一)数组的解构赋值

    let[a,...arr]=[1,2,3,4];//a==>1 arr==>[2,3,4] let [x, y, ...z] = ['a'];//a==>'a' y==>und ...

  8. Beyond Compare 4.X 破解方法(亲测有效)

    Windows下Beyond Compare 4 30天评估到期了的话,可以尝试下面两种方式: 破解方式把Beyond Compare 4安装文件夹下面的BCUnrar.dll文件删掉就行了,但是这种 ...

  9. 解决adb网络连接中出现的“由于目标计算机积极拒绝,无法连接”错误

    在调试一块全志A83T安卓工控板(已root),启动后,安卓系统正常,设置好以太网 的静态IP地址:192.168.1.181,并接好网线,同时开发电脑WIN7系统IP地址 也是129.168.1.x ...

  10. MySQL基础之常用函数

    数学函数的使用 常用数学函数 函数 作用 函数 作用 ceil() 进一取整 abs() 取绝对值 floor() 舍掉小数部分 power() 幂运算 round() 四舍五入 pi() 圆周率 t ...