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

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

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

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. 软件文档写作-plantuml画用例图和时序图

    背景 当下的软件开发人员,不可避免的需要输出一些软件设计文档,作为一个软件工程专业毕业的工程师,最常用的设计工具就是UML,使用UML工具绘制一些软件相关的图,是必备技能,也是输出的技术文档中的重要组 ...

  2. java8-新的日期API

    背景 java的日期和时间API设计不理想,java8引入新的时间和日期API就是为了解决这个问题. 老的日期API的核心类 缺点 Date 月从0开始,年最小从1900年开始,没有时区的概念 Cal ...

  3. vue的provide和inject特性

    由来 组件之间的通信可以通过props和$emit的方式进行通信,但是如果组件之间的关系非常复杂的话,通过以上的方式会很麻烦,并且程序会非常脆弱,没有建中性可言. 在==vue2.2.0 中新增pro ...

  4. 【编译系统02】编译器 - 语义分析器(semantic)的简单设计思路(变量类与变量表)

    当我们分析到 "int n;",说明其已经定义了一个变量,之后又遇到一个 "n=3",我们从哪里去找这个n并且赋值呢? 答案是:通过我们定义的 变量表(Tabl ...

  5. Python迭代器(函数名的应用,新版格式化输出)

    1. 函数名的运用 ​ 你们说一下,按照你们的理解,函数名是什么? ​ 函数名的定义和变量的定义几乎一致,在变量的角度,函数名其实就是一个变量,具有变量的功能:可以赋值:但是作为函数名他也有特殊的功能 ...

  6. Linux-shell学习笔记2

    1.命令的运行顺序 以相对/绝对路径运行命令,例如『 /bin/ls 』或『 ./ls 』: 由 alias 找到该命令来运行: 由 bash 内建的 (builtin) 命令来运行: 通过 $PAT ...

  7. Http相关小知识点笔记咯~

    协议 先来说说什么是协议,协议其实指的是通信协议(Communications Protocol),也称传输协议.Wiki中的描述的是这样的,通信协议定义了通信中的语法学,语义学和同步规则以及可能存在 ...

  8. RESTFUL如何指导WEB API设计?

    博主刚刚接触web开发的时候,写了一个接口 /get_article_info/1 获取id为1的这篇文章的内容,被前辈们看见了,前辈给我说我这个接口设计的不太好啊,不符合RESTFUL规范,当前辈们 ...

  9. linux学习(十)Shell中的控制语句

    目录 1. 条件测试语句 1.1 test语句 1.2[]语句 1.3文件测试 1.4字符串测试 1.5数值测试 1.5逻辑操作符 @(Shell中的控制语句) 1. 条件测试语句 测试语句十Shel ...

  10. 小计C++中的引用和vector

    声明引用的同时必须对其初始化,否则系统会报错,所以我们是无法这样使用 vector<int &> vec; 这条语句会报错,同时引用不是定义一个新的变量或对象,因此内存不会为引用开 ...