事件Event詳解


簡介

事件模型很多編程語言中都有廣泛的應(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ā)流程

在饑荒中,觸發(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 
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • https://nodejs.org/api/documentation.html 工具模塊 Assert 測試 ...
    KeKeMars閱讀 6,620評論 0 6
  • 1.JQuery 基礎(chǔ) 改變web開發(fā)人員創(chuàng)造搞交互性界面的方式。設(shè)計者無需花費時間糾纏JS復(fù)雜的高級特性。 1....
    LaBaby_閱讀 1,515評論 0 2
  • 1.JQuery 基礎(chǔ) 改變web開發(fā)人員創(chuàng)造搞交互性界面的方式。設(shè)計者無需花費時間糾纏JS復(fù)雜的高級特性。 1....
    LaBaby_閱讀 1,277評論 0 1
  • 工廠模式類似于現(xiàn)實生活中的工廠可以產(chǎn)生大量相似的商品,去做同樣的事情,實現(xiàn)同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 8,140評論 2 17
  • 心里癢癢想要寫點東西,下載了簡書。 每每看到誰又辭職了,專職寫文字了,就癢癢地羨慕,但是想不明白,只是寫文字怎么能...
    滄海一粟15閱讀 326評論 2 3

友情鏈接更多精彩內(nèi)容