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

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

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

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. Hibernate 数据层基类实现

    提取经常操作表如新增.修改.删除.查询.分页查询.统计等业务功能,形成基类,用泛型传参,有利于每个实体对象数据层继承. package com.base.dao; import java.io.Ser ...

  2. Windows自动执行应用程序或脚本(可以通过写bat文件定时关机等)

    1. Windows每天定时执行某个应用程序 1.1 右键我的电脑选择管理,并选择任务计划程序,如下 演示 --- 1.2 创建基本任务 演示 1.3 Windows每天定时关机设置参数 演示 1. ...

  3. java之子类对象实例化过程

    假设现在有这么一个父类: public class Person{ public Person(){} public String name = "tom"; public int ...

  4. devops--django+ldap

    首先,什么是Ldap,官方解释 轻型目录访问协议(英文:Lightweight Directory Access Protocol,缩写:LDAP,/ˈɛldæp/)是一个开放的,中立的,工业标准的应 ...

  5. PAT 1006 Sign In and Sign Out 查找元素

    At the beginning of every day, the first person who signs in the computer room will unlock the door, ...

  6. .NET轻松实现支付宝服务窗网页授权并获取用户相关信息

    前言: 最近在开发一个商业街区的聚合扫码支付功能,其中需要用到的有支付宝,微信两种支付方式,当然对于开发微信支付而已作为自己的老本行已经比较熟悉了,然而对于我来说支付宝支付还是头一次涉及到.这次项目中 ...

  7. 爬虫爬取m3u8视频文件

    一.m3u8视频格式 一般m3u8文件和 视频流ts文件放在同一目录 而m3u8文件格式存放的一般都是ts 文件的一个列表 二.根据m3u8视频存放以及写法的规律 思路 我们一般网站上能找到的m3u8 ...

  8. 网络协议 15 - P2P 协议

    大家说起种子,应该都知道是用来下载资源的.那么资源下载都有哪些方式?种子下载又有什么优势呢? 下载电影的两种方式     第一种是通过 HTTP 进行下载.这种方式,有过经历的人应该体会到,当下载文件 ...

  9. 松软科技带你学前端:JavaScript 输出

    JavaScript 不提供任何内建的打印或显示函数. JavaScript 显示方案 JavaScript 能够以不同方式“显示”数据: 使用 window.alert() 写入警告框 使用 doc ...

  10. curl的使用指南

    一.查看网页源码 直接在curl命令后加上网址,就可以看到网页源码.我们以网址www.sina.com为例(选择该网址,主要因为它的网页代码较短): $ curl www.sina.com ​   M ...