按我个人的理解,“事件”在游戏中就是对象的行为,比如主角会有使用道具的行为、炸弹会有爆炸的行为、敌人可以有被攻击的行为,执行这些行为就是触发了事件,触发了一个事件,必定会导致一个结果,例如玩家点击道具按钮(这是一个事件),触发了主角使用炸弹的事件(由前一个事件触发后导致的结果,同时它也是一个被触发的事件),使用炸弹事件触发了炸弹的爆炸事件,炸弹的爆炸事件触发了敌人被攻击事件。这一系列的事件,被程序猿封装成相应对象中一个个的方法,触发事件,其实就是调用相应的方法。上面举例的一系列行为,简化点描述就是:主角攻击敌人,用伪码描述就是:
player.attack(enemy) {
if (enemy) {
enemy.reduceHP();
}
}
假如上面的代码是在主角模块定义的,那么这个模块就得包含敌人模块,这样才能引用敌人模块中的方法。正因为这样,导致了这两个模块间的耦合度变高,一个模块改变,另一个也得跟着改变,例如敌人降低HP的方法名称不是reduceHP()了,那主角模块也得相应变化。模块间必定得有交互的,那么如何降低模块间的耦合度呢?来看下事件机制。
首先来想象下事件系统长什么样:每个对象都可能要访问这个事件系统,因此用单例模式将它定义为一个全局对象,方便使用。事件肯定有很多个,因此需要一个数据结构来存放不同的事件。同时每个事件都应该有一个唯一的名称(一般是用字符串),用来访问对应的事件。还得有个方法用于注册事件,使得触发事件时可以执行对应的事件逻辑。要执行对应事件的逻辑,当然得有个方法用于触发事件(发送消息)。最后,当某些事件不需要了,得有个方法用于注销事件。一个全局事件系统,大概有这几个属性就够了。用伪码描述,它看起来像这样:
// 事件结构
Event {
// 事件名
name: string
// 事件回调,也就是触发该事件后所需执行的逻辑
callback: func
// 事件发送者,可能并不一定用到
sender: object
// 事件接收者,也不一定需要
caller: object
}
// 事件系统
EventSystem {
// 事件系统在全局中的唯一一个实例
instance: EventSystem
// 事件列表,用于存放事件
eventTable: [Event]
// 方法,注册事件
registerEvent(eventName, callback): func
// 方法,发送事件,data参数用于传递数据给回调用
sendEvent(eventName, data): func
// 方法,注销事件中的回调
unregisterEvent(eventName, callback): func
}
简单讲下事件列表,JavaScript中,或很多脚本语言中支持字符串做数组下标,因此可以直接用一个数组来存放事件,不支持这个特性的语言,则可以用哈希表来存放事件(大概那种数组的底层实现,也是哈希表,而哈希表的底层实现就是普通数组,只支持数字做下标的),因为数组和哈希表查找元素的时间复杂度都是O(1)。因为事件可能被频繁触发,因此事件的查找效率必须要高。同一个事件可能需要执行多个逻辑,因此事件列表的每个元素都是一个事件数组,用于注册多个回调。
有了事件系统,就可以用它来降低模块间的耦合度了。如果两个模块间需要交互,那么它们可以共同约定一个事件:拿主角的攻击来举例,主角和敌人两个模块共同约定一个事件,假设叫“HeroAttack”,在敌人模块中,注册一个事件用于监听是否触发了该事件,像这样EventSystem.instance.registerEvent("HeroAttack", enemy.reduceHP)
。主角模块中,每帧更新时检测主角是否发动了攻击,如果有,则发送一次事件,像这样EventSystem.instance.sendEvent("HeroAttack", data)
。如果该事件不需要了,注销即可,像这样EventSystem.instance.unregisterEvent("HeroAttack", enemy.reduceHP)
。
通过上述方式,两个模块间并不需要包含其它模块,也不必关心其它模块中的具体细节,它们只需共同约定一个事件,就可以产生交互,这样就达到了解耦的目的。
事件系统的具体逻辑是这样的:注册事件的方法是将回调存放到eventTable['eventName'][index]
中,假设事件列表是一个数组,事件名做数组下标,数组元素又是一个数组,用来存放同一事件下的不同回调。而发送事件的方法其实就是根据eventName
从事件列表中找到对应的事件,然后遍历调用eventTable[eventName]
这个数组中的所有回调。这样主角每发一次HeroAttack
事件,就会在发送事件那个方法的内部调用对应的enemy.reduceHP
这个回调,于是就完成了两个模块间的交互。刚开始看到“发送事件”这个操作时,我以为是要一个对象发送一条消息给另一个对象,后来发现“发消息”其实就是在方法内部直接调用回调,根本不需要两个对象间进行什么交流…
使用方式的话,CocosCreator、Layabox和Egret引擎中都有,这里就不贴代码了,给下它们的文档链接,里面内容不是很多:CocosCreator监听和发射事件,Layabox自定义事件,Egret事件的执行流程。其中,CocosCreator讲的最简单直接,注册事件用node.on()
(node就是一个节点组件),发送事件用node.emit()
,注销就是node.off()
;Layabox举了个示例,代码多了点,注册也是用sp.on()
(sp就是一个Laya.Sprite
类型的实例),发送用sp.event()
,注销用sp.off()
;Egret详细的讲了个例子,篇幅有点长,它的注册用sp.addEventListener()
(sp是一个egret.Sprite
类型的实例),发送用sp.dispatchEvent()
,注销用sp.removeEventListener()
。其实它们的用法都差不多的。
本想再用C语言写个简单示例的,偷个懒就不写了。用C实现的话,你需要一个哈希表,自己实现或找个现成的,其它就没什么难度了,原理按上面的来就行。
事件系统好像有个更官方的说法,是叫观察者模式吧。这里只是根据自己的理解来描述的,有不正确的地方,还请批评指正。?