pvzclass是如何实现的?pvzclass源代码初步分析(5)Plant类 & Zombie类
PVZ类的成员类,除了Memory类外都大同小异。
它们都有自己的构造函数,使用指针,通过基址+偏移的方法实现修改PVZ对象的功能。
本篇以Plant类和Zombie类为例,解读pvzclass如何实现除Memory类外的成员类。
强烈建议在阅读本篇前先阅读第3篇和第4篇的内容。



虽然有定义成员类的代码,但是PVZ类下并没有定义类型为成员类的变量。以Plant类为例:

同时可以看到,pvzclass中,返回值为成员类的方法,返回的都是指针。
再加上几乎每个成员类都有构造函数,实例化成员类时,建议使用指针和new。
Plant类
Plant类包括的内容,自然是修改PVZ中植物的属性用的。
Plant类的内容可以分为三部分:基址和构造函数、对象属性、方法。
(以下展示的代码不包括原代码中的注释部分)

BaseAddress变量存储的,是Plant对象对应的植物对象在PVZ本体中的内存基址。它的赋值由构造函数完成:

构造函数的参数是"indexoraddress",即"index"或"address"。其中"index"指的是植物对象的序号,不会大于1024;而"address"则是上文提到的,植物对象所在的内存基址,必然大于1024。这种数值分布是PVZ内部决定的。
pvzclass可以依照这条特性确认参数是"index"还是"address",最终确认基址并完成初始化。
DebugType的初始化涉及Plant类的属性,接下来讲。


对象属性部分的代码几乎完全使用INT_PROPERTY宏和T_PROPERTY宏实现。
这些成员变量可以像常规变量一样调用和赋值,并且因为宏定义的调节,可以直接反映到PVZ本体的对应变量上。
使用INT_PROPERTY宏定义的变量,其名称为第一个"参数";使用T_PROPERTY宏定义的变量,其名称为第二个"参数"。
比如,如果你要将某一植物的生命值(即"Hp")改为23333,你的代码应当为:
如果你没能从变量名中推测出变量的作用,可以打开MemoryAddressList,利用变量对应的偏移量(就是最后一个参数),在植物对象的成员变量中找到对应的变量。


使用这些方法可以对植物进行一些操作。
方法的具体定义在Classes文件夹下的"Plant.cpp"中。
GetAnimationPart1()和GetAnimationPart2()可以获取植物对象的动画部件。它们的实现非常简单:

Light()和Flash()的实现也很简单:

查询MemoryAddressList可知,0xB8和0xBC对应的变量分别是发光倒计时和闪光倒计时。
这两个方法的原理也很简单,就是通过修改倒计时实现相应效果。
剩下的四个方法都使用了注入汇编代码的方式实现功能。它们直接调用PVZ内部的函数,以实现一般情况下难以通过修改(或读取)少数变量实现的功能。


在第3篇中已经讲过,SETARG宏用于修改byte数组(也就是待注入的汇编码)中的部分内容,这里用于调整指令用的参数。
这些汇编代码的声明在AsmFuntions.h,定义则在AsmFunctions.cpp中。
Shoot()的架构基本一致,不过因为自身的功能(即立刻令植物发射子弹),代码上有一些不同:

在PVZ中,香蒲的子弹可以跟踪攻击场上的僵尸。Shoot命令的targetid参数可以为生成的子弹指定僵尸作为攻击目标,并完成其他属性的设置。
容易看出,代码的前半段是生成子弹,后半段是设置子弹的跟踪情况。
这里出现了第4篇中提到过的Memory::Variable,它在这里的作用是暂时存储子弹的基址,这个基址最终会被Execute()函数抓取。
因为注意:对杨桃使用Shoot()方法时,pvzclass不会抓取基址,而是直接留空。
这里牵扯到了一些子弹的属性,这个后面会讲。
还剩下一个SetAnimation(),它的代码是这样的:

其中的lstrcpyA()可以视为一种特殊的strcpy()。
看上去和前面几个都不一样。
然而,我们把Execute()的代码拿出来,比对一下:

为什么SetAnimation()没有使用Execute()?
我们看一眼汇编码部分,它长这样:

这一部分相当于如下的代码:
SetAnimation()中有一个动态设置的参数,这一参数根据申请的内存地址进行确定,但PUSHDWORD宏的参数是绝对引用的。
因此SetAnimation()不得不采取改写Execute()的方法。
如果使用pvzclass写汇编代码,需要动态为与申请的内存地址相关的变量赋值,可以考虑用类似的方法来实现。
Zombie类
Zombie类包括的内容用于修改PVZ中僵尸的属性。
类似地,Zombie类的内容可以分为三部分:基址和构造函数、对象属性、方法。


这一部分和Plant类基本一样,跳过。
属性方面,除了Index(僵尸对象序列的基址)意外,依然是用T_PROPERTY宏及其变种定义成员变量。
同样地,你可以打开MemoryAddressList,利用变量对应的偏移量在僵尸对象的成员变量中找到对应的变量,以确认变量对应的僵尸属性。
方法方面,Zombie类包含的方法更多,但原理也就那么几类:




这里出现的CollisionBox(碰撞箱)、AccessoriesType1(I类护甲)和AccessoriesType2(II类护甲)三种结构体,都是在PVZ类之外定义的。定义它们的代码在PVZ.h开头的部分。


Zombie::SetAnimation()和Plant::SetAnimation()相似,都是用类似Execute()的代码完成自身的功能。
各位创作者想要实现类似的功能时,也可以模仿上面的代码,自己编写出同类型功能的代码。

下一篇将解读PVZ类直属的成员变量和直属方法。
阳光数、出怪倒计时等场景性的属性,大部分由直属的成员变量进行调控。
实例化PVZ类成员类的方法,大都是直属方法。
都是地位比较高的内容。
敬请期待。