pvzclass是如何實(shí)現(xiàn)的?pvzclass源代碼初步分析(10)events 組件 上
在 events 組件加入 pvzclass 之前,pvzclass 對 PVZ?內(nèi)部變化的判定實(shí)際上非常局限。
events 組件擴(kuò)充了 pvzclass 的判定功能,方便不擅長高級代碼的創(chuàng)作者實(shí)現(xiàn)復(fù)雜功能(比如:植物受到傷害時魅惑周圍的僵尸)。
events?組件是如何做到的?本篇可以讓你略知一二。
強(qiáng)烈建議在閱讀本篇前先閱讀第3~5篇的內(nèi)容。
注:本文以2021.8.28更新的版本為準(zhǔn)。?包括第51號合并請求的部分內(nèi)容。




events 組件入門教程
雖然本文理應(yīng)只解讀 events 組件的代碼實(shí)現(xiàn),但考慮到 events 組件多次更新,還是先補(bǔ)一份簡單的說明為好。
若想使用 events 組件的功能,你得先定義一個 EventHandler ,如:
(以上摘自第51號合并請求前的 pvzclass.cpp?)
EventHandler 的構(gòu)造函數(shù)需要一個參數(shù):PVZ 類(如以上例子中的 pvz )。
定義 EventHandler?之后,你就可以通過它的成員函數(shù) RegistryListeners()?注冊函數(shù),告訴這個?EventHandler?在什么類型的事件發(fā)生后運(yùn)行什么函數(shù)。
RegistryListeners()?有兩種聲明。第一種聲明通過事件名稱指定事件類型:
(以上摘自 events.h )
另一種聲明在 2021.8.9 更新后加入,通過函數(shù)參數(shù)指定事件類型:
(以上摘自 events.h?)
無論使用何種聲明,注冊的函數(shù)應(yīng)具有如下參數(shù):事件類(可以是任一 Event 類的派生類,但每個函數(shù)只能有一個)和PVZ類。比如如下函數(shù)在“植物死亡"事件發(fā)生后執(zhí)行:
(以上摘自第51號合并請求前的 pvzclass.cpp?)
最后一個參數(shù) Level 表示函數(shù)執(zhí)行的優(yōu)先級,這里暫不詳細(xì)講述。
注冊完所有函數(shù)之后,你就可以調(diào)用成員函數(shù) Run() ,讓?EventHandler?開始干活。
但是調(diào)用一次 Run()?只能讓?EventHandler?運(yùn)行一次。所以你需要反復(fù)調(diào)用 Run() 。
另外,EventHandler?通過高頻比較 PVZ 本體的內(nèi)存變化實(shí)現(xiàn)它的功能,所以你調(diào)用 Run()?的頻率應(yīng)當(dāng)足夠高。
示例代碼干脆直接循環(huán)調(diào)用 Run() :
(以上摘自第51號合并請求前的?pvzclass.cpp?)
講了這么多,終于可以進(jìn)入本文的正題了:分析Events組件。
events.h
events.h 聲明并定義了 events?組件的大部分內(nèi)容,而組件中其他文件完成聲明的具體定義。
開頭的類型定義有一條需要注意:

這里定義的正是你通過?RegistryListeners() 注冊的函數(shù)。
除此之外,events.h?定義了兩個類:記錄事件類型和詳細(xì)信息的 Event?類(及其派生類),以及完成 events?組件功能的?EventHandler?類。
Event類的定義非常簡單:

CancleState?屬性的作用會在下面講到。
name(注意是全小寫)用來區(qū)分不同的派生類,其賦值在派生類的構(gòu)造函數(shù)中完成。
以其中一個派生類為例:

在 2021.8.9 更新前,構(gòu)造函數(shù)直接將字符串常量賦值給 name。但現(xiàn)在字符串常量都放到了 events.cpp?中,通過派生類的 Name?屬性間接賦值。
除了 Name?屬性,大部分派生類都會額外定義一些變量,用來記錄事件涉及的對象。
比如,EventPlantDamage?額外定義了 plant?和?zombie ,用于追蹤受傷的植物及其周圍的僵尸(事件可能會有多個,分別追蹤該植物周圍的每一個僵尸)。
目前 events?組件支持的所有事件可以在 events.cpp?中?通過字符串常量間接查詢:

EventHandler?類部件較多。
private?部分如下圖,包括供成員函數(shù)使用的 PVZ 類指針 pvz 、生成單位列表的三個 GetAll()?函數(shù)、上次?EventHandler?更新后場上的對象列表 …List 、已注冊函數(shù)的列表listeners... 、基址 Address 、兩個作用不大的變量,
以及成員函數(shù) InvokeEvent() (事件產(chǎn)生,觸發(fā)注冊的函數(shù))、Update() (EventHandler?的主函數(shù),檢測事件是否產(chǎn)生并更新對象列表)及其分支。

public?部分包括它的構(gòu)造函數(shù)、析構(gòu)函數(shù),和成員函數(shù) Run() 、?RegistryListeners() 。
上面提到,RegistryListeners()?有兩種聲明,但實(shí)際上第二種聲明(如下圖)……

同時需要注意,這里注冊函數(shù)可以對一整類事件生效,無論你使用哪種聲明的 RegistryListeners()。
如果你對可能涉及到的對象有其他額外要求(比如只對氣球僵尸有效),你需要在將被注冊的函數(shù)中自行添加用于判定的代碼。EventHandler 可不在乎這些。
events.cpp
除去上文提到的字符串常量,events.cpp?還包括 InvokeEvent()?和 RegistryListeners()?兩個成員函數(shù)的定義。
RegistryListeners()?根據(jù)優(yōu)先級將函數(shù)放到不同的 listeners?動態(tài)數(shù)組中:

InvokeEvent()?則負(fù)責(zé)根據(jù)觸發(fā)的事件運(yùn)行已經(jīng)注冊好的函數(shù)。

可以看出,對于注冊時優(yōu)先級相同的函數(shù),先注冊的函數(shù)有更高優(yōu)先級。
這里可以看出 event?的屬性 CancleState?的作用:若 CancleState 為 true,則注冊的函數(shù)在執(zhí)行完畢后立刻返回,不再執(zhí)行優(yōu)先級更低的函數(shù)。
isDelete?表明這次事件觸發(fā)完成后是否會將這個事件釋放,避免內(nèi)存泄漏。
雖然這個參數(shù)存在,但實(shí)際上這個參數(shù)的值(至少 events 組件自身的調(diào)用如此)都是 true 。
不過,如果 InvokeEvent 因?yàn)槭录?CancleState 為真而返回,isDelete 就無法發(fā)揮作用。
這個漏洞的修復(fù)恐怕要等到第53號合并請求了。
分析完了events(.h/.cpp),接下來先分析最簡單的 LevelEvent.cpp。
LevelEvent.cpp
這份文件包括一個變量 wave,用于存儲當(dāng)前波數(shù),以及之前提到過的函數(shù)?UpdateLevels() 。
這個函數(shù)檢測關(guān)卡類事件是否發(fā)生。
關(guān)卡類事件可以分為兩類:切換類(進(jìn)入關(guān)卡、重開、退出關(guān)卡)和進(jìn)行類(關(guān)卡開始,一波僵尸刷新)。
以下是切換類事件相關(guān)的代碼:

其實(shí)原理很簡單:在關(guān)卡內(nèi),BaseAddress?必定非0,否則 BaseAddress?一定為0。關(guān)卡重開時會將 BaseAddress?換為一個新的值。
不過重開事件(即 EventLevelRestart)是2021.8.26更新后才加上的,它是否能正確觸發(fā)我也不大確定(因?yàn)樯衔乃f的更換?BaseAddress?可能先將其設(shè)定為0),可能還藏著一個漏洞。
重開事件和進(jìn)入關(guān)卡事件的觸發(fā)后面,都接著一段刷新對象列表的代碼,這是為了防止讀檔時存檔記錄的對象觸發(fā)各自的“生成”事件(這一事件顯然理應(yīng)已觸發(fā)過)。
進(jìn)行類事件相關(guān)的代碼如下:

原理也很好理解,簡單分析通過 PVZ?類讀取的數(shù)值,確定事件是否發(fā)生,并調(diào)整相關(guān)變量。
這么一看,其實(shí)重開事件觸發(fā)的代碼寫得很倉促,有兩個變量沒有處理。這種瑕疵還是挺不應(yīng)該的。

本篇分析的,大部分是 events 組件的外部框架。
下一篇就要分析 events?組件的內(nèi)核了。
實(shí)際上,events?組件的內(nèi)核不很精致,甚至可以說很粗陋。
詳細(xì)展開,就要等到下篇了。