事件Event详解
简介
事件模型很多编程语言中都有广泛的应用,在饥荒中也一样。许多component在执行一些核心函数时都会顺带触发事件,比如combat在攻击到目标是就会触发onhitother事件。这时候如果监听该事件,就能在完成攻击的同时附加一些特殊效果,比如附带闪电伤害之类的。
一个事件模型有三个组成部分:被监听对象source(也称为事件源),事件event和监听对象listener。
首先,由监听对象注册监听回调函数(Callback),当事件源触发了事件后,监听对象会收到事件源的信息,然后决定如何对事件源进行处理,简要流程如下图所示。
在饥荒中,触发事件的函数是PushEvent(event,data)
。
event是一个字符串类型,指明事件的名字。如果监听对象提前设置监听了这个事件,那么被监听对象触发这个事件时,监听对象就会执行提前设定好的处理函数。
data则是一张表,可以自由选择发生事件时的数据,记录在其中,然后传递给监听对象,也可以直接传递一个对象过去。
设置监听事件的函数是ListenForEvent(event,fn,source)。
event同上。fn是事件处理函数,固定有两个参数,第一个是被监听对象的引用,第二个是传递过来的数据表。source是被监听对象,此参数可以为空。如果为空,则系统会默认source是监听对象自身。也就是自己监听自己。这在饥荒中非常常见。需要特别注意的是,可以通过重复使用ListenForEvent设置多个监听器,即使其参数完全一致也可以。
有时候我们希望监听器只使用若干次,之后不再监听。那么,可以使用RemoveEventCallback(event,fn)来移除监听器。需要注意的是,这里的fn必须首先由ListenForEvent注册过,如果fn未注册,则会崩溃报错。这个函数同样可以重复使用,可以看作是ListenForEvent的逆函数。
以上的几个函数都是EntityScript类的成员函数。所以实际调用时,是这样写的:对象引用名:PushEvent(event,data)
或者对象引用名:ListenForEvent(event,fn, source)
或者 对象引用名:RemoveEventCallback(event,fn)
上面所提到的ListenForEvent是手动设置监听器,但其实还有另外一种特殊的,格式化了的监听器,EventHandler,参数只有event名和fn,没有source(因为已经默认了source就是监听对象自身了)。这个监听器通常是在StateGraph中被使用,用于转换prefab的State,这里不多提,等到以后讲StateGraph时再说。
应用场景
在知道了基本原理之后,我们更关心的还是应用Event的场景。
根据上面的事件触发流程图看到,我们可以通过设置事件触发点(PushEvent),为一个对象设置向外连接的通道。其它的任何对象,都可以通过设置一个监听器(ListenForEvent)来与触发事件的对象建立连接,获得触发事件的对象以及事件数据。这里的对象,虽然限定了只有prefab才能建立连接,但实际上所有的component和大部分widget之类的对象,构造函数就要求传递它所附着的prefab对象,构造函数里也会将传递过来的prefab对象,用一个成员变量接收(一般写作self.inst=inst)。所以要在component或widget中设定监听器也是非常方便的。
下面简单介绍一些常用的场景。
动作事件
游戏里内置了大量的事件触发点(即执行了PushEvent函数),这些触发点通常都写在某个组件的某个函数里。而这些函数通常会在人物执行某个动作(比如攻击,收获,砍树挖矿等)时执行,从而触发事件。这一类事件,我称之为动作事件,它们通常是写在组件的某个执行函数里,而这个执行函数又和动作绑定在一起,这就造成了做动作必定会触发相应事件。
动作事件在饥荒里存在的数量最多,应用最广泛,和prefab自身的关系也最为密切。通常来说,动作事件的事件源都是prefab自身。
典型的动作事件(事件源都是自身)举例:
| 事件| 描述 |data的字段|
| ------------- |-------------||-------------|
| onhitother | 在攻击命中到其它生物时触发 |target:攻击目标的引用,damage:造成的伤害,stimuli:外部刺激,这个参数主要是晨星用的,redirected 重定向目标|
| picksomething| 收获干草、树枝、 、花等等 | object:要收获的目标(也就是种着的草,小树苗或浆果从) loot:收获到的东西 |
需要注意的是,有时候一个动作可能会对应多个事件,也可能没有触发事件。也就是说,动作触发事件不是必须的,这得看组件里的相关函数是怎么写的。如果要对某些不触发事件的动作进行特殊处理,建议看我的另外一篇专门讲动作的文章。
状态事件
状态事件是指prefab有某些状态变化的时候触发的事件,比如,饥饿度变化的时候就会触发hungerdelta事件,这个事件触发得非常频繁,以至于我们可以利用它来做实时监测人物的全部属性。
状态事件有多种用途,像上面说的hungerdelta由于其触发频繁,甚至可以当成一种监测手段,这里说一个比较重要的用途:指示UI变化。比如说,饥饿度指示器可以监听人物的hungerdelta事件,每次监听到该事件后就对指示器的动画进行调整。虽然实际上饥饿度指示器用的是别的方法来达成目的,但这个方法仍然有效。
典型的状态事件
| 事件| 描述 |data|
| ------------- |-------------||-------------|
| hungerdelta| 在饥饿度有变化时触发|oldpercent:变化前的饥饿度百分比, newpercent:新的饥饿度百分比, overtime:几乎用不到的参数,实际意义不明|
| itemget| container组件的触发事件之一。当背包、箱子获得获得物品时,会触发该事件。相应的容器UI会监听到此事件,让相应的格子里显示出东西或者数字发生变化。 | slot:指出是哪个格子获得物品,item:获得什么物品, src_pos:意义不明|
世界变化事件
这类事件也算是状态事件的一种,只是限定了事件源为World。由于它们比较特殊,用得比较广泛,就单独拿出来说。
用ListenForEvent设置监听器来监听世界状态,主要是用于单机版。
在联机版里,对TheWorld这个特殊的对象,官方提供了一个新的更好用的监听器:WatchWorldState。这个监听器的用法和ListenForEvent很相似,但监听的不是事件,而是TheWorld.state的某个状态量(比如cycle-过了多少天,isday-是不是白天),只要这个状态量一发生变化,就会立刻通知监听器执行预先设置的处理函数,传递的数据也不是data,而是这个状态量变化后的值。
可以利用世界变化做很多花样,不过这里限于时间和精力的关系,不能多写,简单地说一下如何找到我们想要的事件。
对单机版,查找事件比较麻烦。但对联机版,可以查看worldstate组件,它的data表下的所有元素都是可以监听的。也可以从这里找到一些世界变化事件的监听器,根据它们监听的事件来对应单机中的相应事件。
应用示例
利用游戏已有的事件
第一步:确定触发场景,然后寻找适合场景的触发事件(PushEvent),确定事件的传入数据。使用Notepad++搜索整个script文件夹,找到具体的事件名。这种查找效率比较低,不过,事件大多是在component中触发的,可以先从一个prefab找到它的component,进而找到事件。
第二步:设置事件监听器,编写代码。
下面来看几个实例
攻击附带冰冻效果
前面说过,在打到攻击目标时,攻击者会触发onhitother事件,可以利用这个来做到。
这是一个典型的监听自身的监听器,不需要设置ListenForEvent的第三个参数。
将以下代码写入人物的fn或master_init中,打开游戏进行测试。
inst:ListenForEvent("onhitother",function(inst,data)
if data and data.target then
local target = data.target
if target.components and target.components.freezable then --只有有freezable组件的prefab才会被冰冻
local coldness = 12 --(冰冻强度,每个可冰冻的prefab都有冰冻抗性,只有积累的强度超过抗性了才会被冰冻)
local freezetime = 10--(冰冻时间)
target.components.freezable:AddColdness(coldness, freezetime)
end
end
end)
更进一步,只有下按键后才会触发冰霜攻击的效果,而且第一次攻击结束后就失去冰霜攻击,除非重新按下按键。
将以下代码写入人物的fn或master_init中,打开游戏进行测试。
local freezeAttack = function(inst,data)
if data and data.target then
local target = data.target
if target.components and target.components.freezable then --只有有freezable组件的prefab才会被冰冻
local coldness = 12 --(冰冻强度,每个可冰冻的prefab都有冰冻抗性,只有积累的强度超过抗性了才会被冰冻)
local freezetime = 10--(冰冻时间)
target.components.freezable:AddColdness(coldness, freezetime)
end
inst:RemoveEventCallback("onhitother",freezeAttack)--执行过一次之后,就移除监听器。
end
end
TheInput:AddKeyDownHandler(KEY_R,function()
inst:ListenForEvent("onhitother",freezeAttack)--每次按下R,都会设置一个监听器。
end)
人物下雨时攻击力加倍
这是一个监听其它对象,但是改变自身状态的事件。
将以下代码写入人物的fn或master_init中,打开游戏进行测试。
单机版-稍后再写。
联机版
local function OnIsRaining(inst, israining)
if israining then --如果在下雨
inst.components.combat.damagemultiplier =2
else
inst.components.combat.damagemultiplier =1
end
end
inst:WatchWorldState("israining", OnIsRaining)
自定义事件
一般来说,自定义事件常用于自定义UI。
比如说,你设置了一个新组件,这个组件的主要属性是一个新的变量:mana。然后你添加了一个widget用于指示这个Mana的剩余量。现在的问题是,怎样在每次mana有变化的时候,把mana的数据传递给widget。一种做法是启动它的自动更新功能,然后在OnUpdate函数里接收数据并修改widget中的显示数据,这就是饥饿度指示器所用的方法。但像mana这种数据,实际上变化得并不像饥饿度那么频繁,所以可以采用事件监听的方式来节约系统资源。为mana组件设置一个控制mana数据变化的DoDelta函数,要修改mana只能通过这个函数来完成。这样,在这个函数中添加一个manadelta事件触发点,就可以在每次mana发生变化的时候,通知UI更新mana的数据了。
示例如下:
此处只做简单示范了如何传递值,没有调整UI的显示。稍后会在我的网盘内上传相应的实例以供参考。
component:mana
Mana = class(function(self,inst)
self.inst=inst
self.maxMana = 100--最大值
self.current = self.maxMana
end)
function Mana:DoDelta(delta)
local newMana = self.current+delta
local newPercent = newMana/self.maxMana
self.current = newMana
self.inst:PushEvent(manadelta,{current = newMana,npercent = newPercent})
end
return Mana
widget:manaIndicator
local Widget = require "widgets/widget"
ManaIndicator = Class(Widget, function(self, owner)
Widget._ctor(self, "ManaIndicator")
self.owner = owner
self.mana = 100
self.percent = 1
self.owner:ListenForEvent("manadelta",function(inst,data)
self:onManaDelta(inst,data)
end)
end)
function ManaIndicator:onManaDelta(inst,data)
self.mana = data.current
self.percent = npercent
end
return ManaIndicator