设想这样一个情景。在游戏中突然下起了雨,你身边的两个NPC——小红撑起了伞,小明躲到了附近的屋檐下;过一会雨停了,于是小红把伞收了起来,小明走出了屋檐下。
在这个情景中,“下雨”这个事件不是以某个NPC作为对象的,它的发生和NPC并没有什么直接联系。你当然可以在NPC里用代码将它们联系起来,但是当一个事件联系很多个对象,或者判断这个事件可能引发的反应十分复杂时,代码量会骤增而且很难修改。那么怎样让一个事件发生时,有多个对象对它做出反应呢?事件系统出场。
首先我们需要创建一个物体(object)充当事件管理员。管理员手上有一个map容器,里面的键(key)是一个个事件,而每个事件对应的值(value)是一个列表(list),列表中的元素代表正在监听该事件的物体。被监听的事件需要预先放入map,监听者也需要预先放入对应事件的列表中,即“注册”。
当一个事件发生后,事件管理员将会得知,然后它会检查map。如果这个事件在map里面,管理员就会通知监听者执行相应的代码。
教程中使用了三个脚本函数实现功能,分别是:
1.注册事件和监听者
2.事件发生后通知管理员
3.注销监听者和事件
接下来按步骤实现功能。
一、创建EventManager物体
这个物体的主要作用就是管理map里的事件和监听者,然后及时清理不需要的数据结构。
在它的create事件中创建一个map。
eventMap = ds_map_create();
map的内部结构大致如下:
刚创建的map还是空空的,需要我们先向里面加入事件和监听者。
二、注册事件和监听者
事件和监听者的注册在这里用一个脚本函数一起实现了:event_register_script(event, id, script)
参数说明:
event:代表事件,可以是一个字符串代表的名字,也可以是一个枚举列表里的元素,总之应该保证之后能查找到就可以
id:监听者的ID
script:一个长度不定的数组,第一个元素是事件发生后监听者将要执行的代码,其后的元素是该代码的参数(可以没有)。
with (obj_event_manager) { var ev = argument[0]; var objID = argument[1]; // 检查事件是否已经注册 if (!ds_map_exists(eventMap, ev)) { // 若否,先注册该事件 var listenerList = ds_list_create(); ds_map_add_list(eventMap, ev, listenerList); } else { // 若是,直接找到该事件的监听者列表 var listenerList = eventMap[? ev]; } // 读取要注册的监听者信息 var listenerInfo; // 这是个数组 listenerInfo[0] = objID; // 循环读取script参数组(因长度不定) var len = argument_count - 2; var i = 0; repeat(len) { listenerInfo[i+1] = argument[i+2]; i++; } // 将新注册的监听者加入列表 ds_list_add(listenerList, listenerInfo); }
由此可以看到,监听者列表中的每一个元素又是一个数组(array),数组中的元素包含ID,要执行的代码及代码参数。
例如下雨事件的监听者列表可能是这样
注意的点:
1.在map中查找指定键,可以用函数ds_map_find_value(map, key),也可以简写成mapName[? key]
2.F1文档提到,新加入map中的键顺序是不确定的,所以查找指定键时,可能出现遍历整个map才能找到的情况,这个过程会很慢。所以map可能不宜太大。
3.可以用ds_map_add添加监听者列表,但是使用这种方式添加进map的数据结构不会随着map的销毁而自动销毁,所以在销毁map之前,还需要手动遍历销毁其中的元素。若使用ds_map_add_list添加列表,则列表会随着map的销毁而自动销毁,但F1文档中写了这个函数是为适用于JSON格式数据的map打造的,但FC说在这里用也没事。我对JSON也不熟悉,就暂时也这么用吧。
三、事件发生后通知管理员
event_fire(event)
参数说明:
event:代表事件,和上面统一类型
首先检查事件是否已经注册,如果都没注册,管理员也不用理它了。
if (ds_map_exists(eventMap, ev)) { // 判断监听者…… }
然后遍历该事件的监听者列表,对于每一个监听者获取信息,然后让它执行代码。
listenerInfo = listenerList[| i]; listener = listenerList[0]; // ID script = listenerList[1]; // 代码 var lenArgs = array_length_1d(listenerInfo-2); // 代码参数个数 var unregister = false; // 是否在代码执行完成后注销该监听者
这里的unregister是用来判断代码执行完成后是否注销监听者的,由代码的返回值决定。当然如果这个监听者物体这时甚至都不存在了,代码肯定也执行不成(会报错),这时就直接将unregister设置为true。
// 没有参数的情况 if (lenArgs <= 0) { if (instance_exists(listener)) { // 防止监听者已经销毁的情况 with(listener) unregister = script_execute(script); // 由代码返回值决定是否注销 } else { unregister = true; // 注销 } }
有参数的情况同理,但是需要自己写一个script_excute_alt函数,实现能够传入参数组的script_excute。
执行完了以后判断注销。
if (unregister) { event_unregister(ev, listener); i--; // 防止跳过 }
这里的i--是因为注销当前监听者后,列表里后面的元素编号会往前挪一位,此时直接i++操作下一个监听者的话就会跳过一个监听者。
例如这里注销的是2号监听者,下一个操作的应该是3号。但由于2号注销了,3号就变成了2号,4号就变成了3号,此时i++操作的就是现在的3号,原来的4号监听者。原来的3号监听者就被跳过了。
四、注销监听者和事件
event_unregister(event,id)
参数说明:
event:代表事件
id:想要注销的监听者ID
首先还是要检查下这个事件是否已经注册,然后找到它的监听者列表,再在列表里找到对应ID的监听者。
if (listenerInfo[0] == objID) { // 找到ID // 若只剩这一个监听者 if (len == 1) { ds_list_destroy(listenerList); ds_map_delete(eventMap, i); //从map中删除该事件 } else { ds_list_delete(listenerList, i); } break; }
五、销毁map
用完数据结构记得即时销毁!由于前面用的ds_map_add_list,所以这里直接销毁map就好。
ds_map_destroy(eventMap);
————————
高估了我的速度,跟着视频敲一遍代码,又在这里写一遍思路,花的时间不知道翻了好多倍……如果一天写一篇肯定不能写这样的
GMS2的教程真的都是给一个例子,然后跟着做。但是怎么把各个系统整合进一个游戏里,感觉还是一头雾水。先捡各种碎片看看吧。
暂无关于此日志的评论。