簡介
事件模型很多編程語言中都有廣泛的應(yīng)用,在饑荒中也一樣。許多component在執(zhí)行一些核心函數(shù)時都會順帶觸發(fā)事件,比如combat在攻擊到目標(biāo)是就會觸發(fā)onhitother事件。這時候如果監(jiān)聽該事件,就能在完成攻擊的同時附加一些特殊效果,比如附帶閃電傷害之類的。
一個事件模型有三個組成部分:被監(jiān)聽對象source(也稱為事件源),事件event和監(jiān)聽對象listener。
首先,由監(jiān)聽對象注冊監(jiān)聽回調(diào)函數(shù)(Callback),當(dāng)事件源觸發(fā)了事件后,監(jiān)聽對象會收到事件源的信息,然后決定如何對事件源進行處理,簡要流程如下圖所示。

在饑荒中,觸發(fā)事件的函數(shù)是PushEvent(event,data)。
event是一個字符串類型,指明事件的名字。如果監(jiān)聽對象提前設(shè)置監(jiān)聽了這個事件,那么被監(jiān)聽對象觸發(fā)這個事件時,監(jiān)聽對象就會執(zhí)行提前設(shè)定好的處理函數(shù)。
data則是一張表,可以自由選擇發(fā)生事件時的數(shù)據(jù),記錄在其中,然后傳遞給監(jiān)聽對象,也可以直接傳遞一個對象過去。
設(shè)置監(jiān)聽事件的函數(shù)是ListenForEvent(event,fn,source)。
event同上。fn是事件處理函數(shù),固定有兩個參數(shù),第一個是被監(jiān)聽對象的引用,第二個是傳遞過來的數(shù)據(jù)表。source是被監(jiān)聽對象,此參數(shù)可以為空。如果為空,則系統(tǒng)會默認(rèn)source是監(jiān)聽對象自身。也就是自己監(jiān)聽自己。這在饑荒中非常常見。需要特別注意的是,可以通過重復(fù)使用ListenForEvent設(shè)置多個監(jiān)聽器,即使其參數(shù)完全一致也可以。
有時候我們希望監(jiān)聽器只使用若干次,之后不再監(jiān)聽。那么,可以使用RemoveEventCallback(event,fn)來移除監(jiān)聽器。需要注意的是,這里的fn必須首先由ListenForEvent注冊過,如果fn未注冊,則會崩潰報錯。這個函數(shù)同樣可以重復(fù)使用,可以看作是ListenForEvent的逆函數(shù)。
以上的幾個函數(shù)都是EntityScript類的成員函數(shù)。所以實際調(diào)用時,是這樣寫的:對象引用名:PushEvent(event,data) 或者對象引用名:ListenForEvent(event,fn, source) 或者 對象引用名:RemoveEventCallback(event,fn)
上面所提到的ListenForEvent是手動設(shè)置監(jiān)聽器,但其實還有另外一種特殊的,格式化了的監(jiān)聽器,EventHandler,參數(shù)只有event名和fn,沒有source(因為已經(jīng)默認(rèn)了source就是監(jiān)聽對象自身了)。這個監(jiān)聽器通常是在StateGraph中被使用,用于轉(zhuǎn)換prefab的State,這里不多提,等到以后講StateGraph時再說。
應(yīng)用場景
在知道了基本原理之后,我們更關(guān)心的還是應(yīng)用Event的場景。
根據(jù)上面的事件觸發(fā)流程圖看到,我們可以通過設(shè)置事件觸發(fā)點(PushEvent),為一個對象設(shè)置向外連接的通道。其它的任何對象,都可以通過設(shè)置一個監(jiān)聽器(ListenForEvent)來與觸發(fā)事件的對象建立連接,獲得觸發(fā)事件的對象以及事件數(shù)據(jù)。這里的對象,雖然限定了只有prefab才能建立連接,但實際上所有的component和大部分widget之類的對象,構(gòu)造函數(shù)就要求傳遞它所附著的prefab對象,構(gòu)造函數(shù)里也會將傳遞過來的prefab對象,用一個成員變量接收(一般寫作self.inst=inst)。所以要在component或widget中設(shè)定監(jiān)聽器也是非常方便的。
下面簡單介紹一些常用的場景。
動作事件
游戲里內(nèi)置了大量的事件觸發(fā)點(即執(zhí)行了PushEvent函數(shù)),這些觸發(fā)點通常都寫在某個組件的某個函數(shù)里。而這些函數(shù)通常會在人物執(zhí)行某個動作(比如攻擊,收獲,砍樹挖礦等)時執(zhí)行,從而觸發(fā)事件。這一類事件,我稱之為動作事件,它們通常是寫在組件的某個執(zhí)行函數(shù)里,而這個執(zhí)行函數(shù)又和動作綁定在一起,這就造成了做動作必定會觸發(fā)相應(yīng)事件。
動作事件在饑荒里存在的數(shù)量最多,應(yīng)用最廣泛,和prefab自身的關(guān)系也最為密切。通常來說,動作事件的事件源都是prefab自身。
典型的動作事件(事件源都是自身)舉例:
| 事件| 描述 |data的字段|
| ------------- |-------------||-------------|
| onhitother | 在攻擊命中到其它生物時觸發(fā) |target:攻擊目標(biāo)的引用,damage:造成的傷害,stimuli:外部刺激,這個參數(shù)主要是晨星用的,redirected 重定向目標(biāo)|
| picksomething| 收獲干草、樹枝、 、花等等 | object:要收獲的目標(biāo)(也就是種著的草,小樹苗或漿果從) loot:收獲到的東西 |
需要注意的是,有時候一個動作可能會對應(yīng)多個事件,也可能沒有觸發(fā)事件。也就是說,動作觸發(fā)事件不是必須的,這得看組件里的相關(guān)函數(shù)是怎么寫的。如果要對某些不觸發(fā)事件的動作進行特殊處理,建議看我的另外一篇專門講動作的文章。
狀態(tài)事件
狀態(tài)事件是指prefab有某些狀態(tài)變化的時候觸發(fā)的事件,比如,饑餓度變化的時候就會觸發(fā)hungerdelta事件,這個事件觸發(fā)得非常頻繁,以至于我們可以利用它來做實時監(jiān)測人物的全部屬性。
狀態(tài)事件有多種用途,像上面說的hungerdelta由于其觸發(fā)頻繁,甚至可以當(dāng)成一種監(jiān)測手段,這里說一個比較重要的用途:指示UI變化。比如說,饑餓度指示器可以監(jiān)聽人物的hungerdelta事件,每次監(jiān)聽到該事件后就對指示器的動畫進行調(diào)整。雖然實際上饑餓度指示器用的是別的方法來達成目的,但這個方法仍然有效。
典型的狀態(tài)事件
| 事件| 描述 |data|
| ------------- |-------------||-------------|
| hungerdelta| 在饑餓度有變化時觸發(fā)|oldpercent:變化前的饑餓度百分比, newpercent:新的饑餓度百分比, overtime:幾乎用不到的參數(shù),實際意義不明|
| itemget| container組件的觸發(fā)事件之一。當(dāng)背包、箱子獲得獲得物品時,會觸發(fā)該事件。相應(yīng)的容器UI會監(jiān)聽到此事件,讓相應(yīng)的格子里顯示出東西或者數(shù)字發(fā)生變化。 | slot:指出是哪個格子獲得物品,item:獲得什么物品, src_pos:意義不明|
世界變化事件
這類事件也算是狀態(tài)事件的一種,只是限定了事件源為World。由于它們比較特殊,用得比較廣泛,就單獨拿出來說。
用ListenForEvent設(shè)置監(jiān)聽器來監(jiān)聽世界狀態(tài),主要是用于單機版。
在聯(lián)機版里,對TheWorld這個特殊的對象,官方提供了一個新的更好用的監(jiān)聽器:WatchWorldState。這個監(jiān)聽器的用法和ListenForEvent很相似,但監(jiān)聽的不是事件,而是TheWorld.state的某個狀態(tài)量(比如cycle-過了多少天,isday-是不是白天),只要這個狀態(tài)量一發(fā)生變化,就會立刻通知監(jiān)聽器執(zhí)行預(yù)先設(shè)置的處理函數(shù),傳遞的數(shù)據(jù)也不是data,而是這個狀態(tài)量變化后的值。
可以利用世界變化做很多花樣,不過這里限于時間和精力的關(guān)系,不能多寫,簡單地說一下如何找到我們想要的事件。
對單機版,查找事件比較麻煩。但對聯(lián)機版,可以查看worldstate組件,它的data表下的所有元素都是可以監(jiān)聽的。也可以從這里找到一些世界變化事件的監(jiān)聽器,根據(jù)它們監(jiān)聽的事件來對應(yīng)單機中的相應(yīng)事件。
應(yīng)用示例
利用游戲已有的事件
第一步:確定觸發(fā)場景,然后尋找適合場景的觸發(fā)事件(PushEvent),確定事件的傳入數(shù)據(jù)。使用Notepad++搜索整個script文件夾,找到具體的事件名。這種查找效率比較低,不過,事件大多是在component中觸發(fā)的,可以先從一個prefab找到它的component,進而找到事件。
第二步:設(shè)置事件監(jiān)聽器,編寫代碼。
下面來看幾個實例
攻擊附帶冰凍效果
前面說過,在打到攻擊目標(biāo)時,攻擊者會觸發(fā)onhitother事件,可以利用這個來做到。
這是一個典型的監(jiān)聽自身的監(jiān)聽器,不需要設(shè)置ListenForEvent的第三個參數(shù)。
將以下代碼寫入人物的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)
更進一步,只有下按鍵后才會觸發(fā)冰霜攻擊的效果,而且第一次攻擊結(jié)束后就失去冰霜攻擊,除非重新按下按鍵。
將以下代碼寫入人物的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)--執(zhí)行過一次之后,就移除監(jiān)聽器。
end
end
TheInput:AddKeyDownHandler(KEY_R,function()
inst:ListenForEvent("onhitother",freezeAttack)--每次按下R,都會設(shè)置一個監(jiān)聽器。
end)
人物下雨時攻擊力加倍
這是一個監(jiān)聽其它對象,但是改變自身狀態(tài)的事件。
將以下代碼寫入人物的fn或master_init中,打開游戲進行測試。
單機版-稍后再寫。
聯(lián)機版
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。
比如說,你設(shè)置了一個新組件,這個組件的主要屬性是一個新的變量:mana。然后你添加了一個widget用于指示這個Mana的剩余量?,F(xiàn)在的問題是,怎樣在每次mana有變化的時候,把mana的數(shù)據(jù)傳遞給widget。一種做法是啟動它的自動更新功能,然后在OnUpdate函數(shù)里接收數(shù)據(jù)并修改widget中的顯示數(shù)據(jù),這就是饑餓度指示器所用的方法。但像mana這種數(shù)據(jù),實際上變化得并不像饑餓度那么頻繁,所以可以采用事件監(jiān)聽的方式來節(jié)約系統(tǒng)資源。為mana組件設(shè)置一個控制mana數(shù)據(jù)變化的DoDelta函數(shù),要修改mana只能通過這個函數(shù)來完成。這樣,在這個函數(shù)中添加一個manadelta事件觸發(fā)點,就可以在每次mana發(fā)生變化的時候,通知UI更新mana的數(shù)據(jù)了。
示例如下:
此處只做簡單示范了如何傳遞值,沒有調(diào)整UI的顯示。稍后會在我的網(wǎng)盤內(nèi)上傳相應(yīng)的實例以供參考。
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