欢迎光临散文网 会员登陆 & 注册

pvzclass是如何实现的?pvzclass源代码初步分析(10)events 组件 上

2021-09-21 22:07 作者:__W1thoutD0ubt  | 我要投稿

在 events 组件加入 pvzclass 之前,pvzclass 对 PVZ 内部变化的判定实际上非常局限。

events 组件扩充了 pvzclass 的判定功能,方便不擅长高级代码的创作者实现复杂功能(比如:植物受到伤害时魅惑周围的僵尸)。

events 组件是如何做到的?本篇可以让你略知一二。

强烈建议在阅读本篇前先阅读第3~5篇的内容

注:本文以2021.8.28更新的版本为准。 包括第51号合并请求的部分内容。

events 组件入门教程

虽然本文理应只解读 events 组件的代码实现,但考虑到 events 组件多次更新,还是先补一份简单的说明为好。

若想使用 events 组件的功能,你得先定义一个 EventHandler ,如:

(以上摘自第51号合并请求前的 pvzclass.cpp )

EventHandler 的构造函数需要一个参数:PVZ 类(如以上例子中的 pvz )。

定义 EventHandler 之后,你就可以通过它的成员函数 RegistryListeners() 注册函数,告诉这个 EventHandler 在什么类型的事件发生运行什么函数。

RegistryListeners() 有两种声明。第一种声明通过事件名称指定事件类型:

(以上摘自 events.h )

另一种声明在 2021.8.9 更新后加入,通过函数参数指定事件类型:

(以上摘自 events.h )

无论使用何种声明,注册的函数应具有如下参数:事件类(可以是任一 Event 类的派生类,但每个函数只能有一个)和PVZ类。比如如下函数在“植物死亡"事件发生后执行:

(以上摘自第51号合并请求前的 pvzclass.cpp )

最后一个参数 Level 表示函数执行的优先级,这里暂不详细讲述。

注册完所有函数之后,你就可以调用成员函数 Run() ,让 EventHandler 开始干活。

但是调用一次 Run() 只能让 EventHandler 运行一次。所以你需要反复调用 Run() 。

另外,EventHandler 通过高频比较 PVZ 本体的内存变化实现它的功能,所以你调用 Run() 的频率应当足够高。

示例代码干脆直接循环调用 Run() :

(以上摘自第51号合并请求前的 pvzclass.cpp )

讲了这么多,终于可以进入本文的正题了:分析Events组件。

events.h

events.h 声明并定义了 events 组件的大部分内容,而组件中其他文件完成声明的具体定义。

开头的类型定义有一条需要注意:


这里定义的正是你通过 RegistryListeners() 注册的函数。

除此之外,events.h 定义了两个类:记录事件类型和详细信息的 Event 类(及其派生类),以及完成 events 组件功能的 EventHandler 类。

Event类的定义非常简单:

CancleState 属性的作用会在下面讲到。

name(注意是全小写)用来区分不同的派生类,其赋值在派生类的构造函数中完成。

以其中一个派生类为例:

“植物受伤”事件

在 2021.8.9 更新前,构造函数直接将字符串常量赋值给 name。但现在字符串常量都放到了 events.cpp 中,通过派生类的 Name 属性间接赋值。

除了 Name 属性,大部分派生类都会额外定义一些变量,用来记录事件涉及的对象。

比如,EventPlantDamage 额外定义了 plant 和 zombie ,用于追踪受伤的植物及其周围的僵尸(事件可能会有多个,分别追踪该植物周围的每个僵尸)。

目前 events 组件支持的所有事件可以在 events.cpp 中 通过字符串常量间接查询:

截至2021.8.28更新

EventHandler 类部件较多。

private 部分如下图,包括供成员函数使用的 PVZ 类指针 pvz 、生成单位列表的三个 GetAll() 函数、上次 EventHandler 更新后场上的对象列表 …List 、已注册函数的列表listeners... 、基址 Address 、两个作用不大的变量,

以及成员函数 InvokeEvent() (事件产生,触发注册的函数)、Update() (EventHandler 的主函数,检测事件是否产生并更新对象列表)及其分支。

public 部分包括它的构造函数、析构函数,和成员函数 Run() 、 RegistryListeners() 。

上面提到,RegistryListeners() 有两种声明,但实际上第二种声明(如下图)……

第一种声明的套皮罢了

同时需要注意,这里注册函数可以对一整类事件生效,无论你使用哪种声明的 RegistryListeners()。

如果你对可能涉及到的对象有其他额外要求(比如只对气球僵尸有效),你需要在将被注册的函数中自行添加用于判定的代码。EventHandler 可不在乎这些。

events.cpp

除去上文提到的字符串常量,events.cpp 还包括 InvokeEvent() 和 RegistryListeners() 两个成员函数的定义。

RegistryListeners() 根据优先级将函数放到不同的 listeners 动态数组中:

InvokeEvent() 则负责根据触发的事件运行已经注册好的函数。

可以看出,对于注册时优先级相同的函数,先注册的函数有更高优先级。

这里可以看出 event 的属性 CancleState 的作用:若 CancleState 为 true,则注册的函数在执行完毕后立刻返回,不再执行优先级更低的函数。

isDelete 表明这次事件触发完成后是否会将这个事件释放,避免内存泄漏。

虽然这个参数存在,但实际上这个参数的值(至少 events 组件自身的调用如此)都是 true 。

不过,如果 InvokeEvent 因为事件的 CancleState 为真而返回,isDelete 就无法发挥作用

这个漏洞的修复恐怕要等到第53号合并请求了。

分析完了events(.h/.cpp),接下来先分析最简单的 LevelEvent.cpp。

LevelEvent.cpp

这份文件包括一个变量 wave,用于存储当前波数,以及之前提到过的函数 UpdateLevels() 。

这个函数检测关卡类事件是否发生。

关卡类事件可以分为两类:切换类(进入关卡、重开、退出关卡)和进行类(关卡开始,一波僵尸刷新)。

以下是切换类事件相关的代码:

其实原理很简单:在关卡内,BaseAddress 必定非0,否则 BaseAddress 一定为0。关卡重开时会将 BaseAddress 换为一个新的值。

不过重开事件(即 EventLevelRestart)是2021.8.26更新后才加上的,它是否能正确触发我也不大确定(因为上文所说的更换 BaseAddress 可能先将其设定为0),可能还藏着一个漏洞。

重开事件和进入关卡事件的触发后面,都接着一段刷新对象列表的代码,这是为了防止读档时存档记录的对象触发各自的“生成”事件(这一事件显然理应已触发过)。

进行类事件相关的代码如下:

原理也很好理解,简单分析通过 PVZ 类读取的数值,确定事件是否发生,并调整相关变量。

这么一看,其实重开事件触发的代码写得很仓促,有两个变量没有处理。这种瑕疵还是挺不应该的。

本篇分析的,大部分是 events 组件的外部框架。

下一篇就要分析 events 组件的内核了。

实际上,events 组件的内核不很精致,甚至可以说很粗陋。

详细展开,就要等到下篇了。

pvzclass是如何实现的?pvzclass源代码初步分析(10)events 组件 上的评论 (共 条)

分享到微博请遵守国家法律