植僵键控工具PCC食用指南——使用篇

指南目录

前言

在看过了上一篇指南后,我们都已经知道怎样安装PCC和其他一些相关的东西了。在这篇指南中,我们要一起学习如何去使用PCC,怎样用PCC打造你的植僵键控。我相信,看完这篇指南后,大家都能明白怎样去使用PCC了。

使用流程

编写脚本

首先,我们要进行流程的第一步——编写脚本。我们可以选择试试示例脚本,也可以选择编写一个自己的键控脚本。个人推荐先试一试示例脚本,看看效果,熟悉一下环境,再编写自己的键控脚本。

如何使用示例脚本

请注意,所有示例脚本都默认出怪方式为全难度极限出怪(即上一篇指南中演示修改器功能时所展示的出怪方式)。

首先,我们于PCC的安装目录下找到examples文件夹,在该文件夹中选择一个自己喜欢的示例脚本并复制(这里以“【PE】经典十二炮.cc”为例),接着退回到上一层文件夹中(即PCC的安装目录)并粘贴,这样我们就获得了一份示例代码的副本。将该副本的名称改为pvz_controller_by_cpp.cc(结果如下图)。注意,一个字也不能错,文件名是全部小写的,后缀名是.cc。

展示创建键控脚本后的PCC安装目录

这样我们就完成了“编写脚本”这一步骤全部的工作,可以直接移步“生成键控程序”进行下一步工作了。

如何编写自己的键控脚本

强烈建议各位先试一试示例脚本再来编写自己的键控脚本,能有效提高使用体验。

复制脚本模板

首先于PCC的安装目录中找到“键控脚本模板.cc”并复制,在原地粘贴,这样我们就得到了一份脚本模板的副本。将文件名改为pvz_controller_by_cpp.cc,结果如下图。注意,一个字也不能错,文件名是全部小写的,后缀名是.cc。

展示创建键控脚本后的PCC安装目录
如何关闭自动选卡功能

在本脚本模板中集成了自动选卡功能,解放双手。但由于自动选卡有一定几率会失败,并且打开自动选卡后就需要在打开键控程序后5秒内切换至游戏界面否则会失败(手残党哭泣),所以我也提供了关闭自动选卡功能的功能,只需要像下面一样操作就行。

第一步,在main()函数中找到“ChoosingCards();”,如下图:

演示如何关闭自动选卡(第一步)

第二步,在这一行最前面加上“//”(两个斜杠,不包含双引号,如下图),或者干脆将这一行删去,完成后保存文件。

演示如何关闭自动选卡(第二步)

这样,我们就关闭了自动选卡功能。如果想要重新打开的话,将该行最前面的“//”删去或将这一行添加回来就行。

一些可能会用到的函数以及它们的作用
Sleep()

用这个函数可以将程序暂停一段时间,一般可以用来对轴。参数是要暂停的时间,以毫秒为单位。比如 Sleep(500) 就是将程序暂停0.5s,Sleep(1000) 就是将程序暂停1s。有两个需要注意的点:第一个是Sleep()函数的参数是以毫秒为单位;第二个是Sleep()函数的作用是将键控程序暂停一段时间,而不是将植僵游戏暂停,一定要记得!

InitController()

用在键控脚本开头,作用是初始化键控工具。参数有十个,第三个、第四个和第六个不需要在意。第一个参数是场地,泳池是“’P’”,白天是“’D’”,以此类推;第二个参数是这个阵中玉米加农炮的数量,有八门炮就是8,有十二门炮就是12,有二十四门炮就是24,以此类推。

第五个参数是该阵中一共有几个存冰位,没有就写0;第七个参数是你要用用模仿咖啡豆还是普通咖啡豆,普通填false,模仿填true;第八个是在植物选卡时咖啡豆是第几个选的,没选就填0;第九个参数是寒冰菇是第几个选的,没选就填0;第十个是模仿寒冰菇是第几个选的,没选就填0。

举个栗子,如果我在编一个泳池十二炮炮阵的键控脚本,该阵中没有存冰位,选卡时寒冰菇是第一个选的,模仿寒冰菇是第二个选的,普通咖啡豆是第三个选的,那么最开头的InitController()函数应该是这样子的(第三个、第四个和第六个参数不能改也不用管,是固定的):

InitController('P', 12, kCannonList, &used_cannons, 0, kIceList, false, 3, 1, 2);

有一个需要注意的点:第一个参数千万别把单引号给漏了,而且千万别忘了是半角的单引号不是全角的单引号!

CountDown()

这个函数可以返回距离下一波僵尸来临还有多长时间,返回值以10ms为单位。一般来讲这个函数用的不是很多,用的比较多的是封装了一遍的PreJudge()函数。有两个需要注意的点:一个是返回值是以10ms为单位,要与Sleep()函数参数的以1ms为单位区别开;另一个是如果下一波僵尸是大波的话(第10波和第20波),那么返回的值会以红字提示(就是“一大波僵尸即将来袭”那个)出现的时间为该波的开始时间而不是以僵尸出现的时间作为该波的开始时间进行计算,使用时请自行判断处理。

Cannon()

这个函数的作用是向某个位置发射一发玉米加农炮。有三个参数,有用的只有前两个:第一个是炮落点的横坐标,第二个是纵坐标(都支持小数)。如果想往第2行、第9列发一发玉米加农炮,那么应该这样写(第三个参数是固定的,不能改):

Cannon(2, 9, used_cannons);

需要注意的只有一个点:键控不会对游戏做任何修改,也就是说Cannon()函数没法凭空发炮,必须在场上存在可用的玉米加农炮且玉米加农炮的位置被正确写入程序中(下文会讲怎么写入)的情况下才能发出炮来。若是使用Cannon()时场上所有的炮都进入冷却了的话,那么什么事都不会发生,程序会直接执行下一条指令(与RecoverCannon()有所区别)。

Card()

可以在在战斗界面中选中指定的植物卡,搭配Pnt()函数可以将其种植至场上指定位置。只有一个参数,即选定的植物卡的编号。在战斗界面中将所有植物卡从左到右从1到10进行编号(铲子的编号是12),每个植物对应的数字就是它的编号,如下图。

展示战斗界面植物卡的编号

如果想要选中向日葵,先看向日葵是第几个被选的。由图可知在这个例子中向日葵是第十个被选的,编号为10,那么我们输入下面的代码就可以选中向日葵了:

Card(10);

有三个需要注意的点:一是Card()函数仅仅是选中了植物卡,并不会进行任何多余的操作,如果想要种植植物还需要搭配函数Pnt()使用;二是每个植物对应的编号并不是固定的,在这一局中向日葵是第十个选的,那么它对应的编号就是10,下一局小喷菇是第十个选的,向日葵是第二个选的,那向日葵对应的编号就是2,小喷菇对应的编号是10,以此类推;三是如果想要选中铲子的话可以用Card(12),但是有很小的几率会失败,我也不知道为什么QaQ

Click()

这个很好理解,就是用鼠标在后台点一下指定的位置。这个函数有三个参数,前两个是要点的位置在植僵客户端中的坐标,第三个是选择是左键点击还是右键点击(左键点击是false,右键是true)。这个函数平常应该不太会用到,不用太在意。

ClickButton()

这个跟ClickForgnd()基本上是一模一样,只是延长了鼠标落下和抬起的时间,有些按钮点太快了它根本就识别不上,只能又搞了个新函数。这个函数平常更是用不到,可以不用管它。

ClickForgnd()

与Click()类似,只不过是前台点击,除了自动选卡以外没有可以用到的地方了。自动选卡如果用后台点击的话会出一些奇怪的错误,所以就把前台点击功能保留下来了。

ChooseCard()

在选卡界面选择指定的植物卡。有三个参数,第一个是要选的植物卡在第几行,第二个是要选的植物卡在第几列,第三个是确定是否要选择模仿者(false选择普通植物,true选择模仿者)。比如我想选择火爆辣椒(在第3行第9列),那么我可以输入以下的代码:

ChooseCard(3, 5, false);

如果我想选择模仿者寒冰菇(在第2行第7列),那么我可以输入以下的代码:

ChooseCard(2, 7, true);

当然,平常我们是很难用到这个函数的,因为它被集成到自动选卡的函数ChoosingCards()里了。至于如何使用自动选卡,下文会有详细的讲解。

LetsRock()

点击游戏开始按钮,基本用不到,因为被集成到ChoosingCards()里了。

PreJudge()

该函数的作用是暂停程序直到指定的时间为止。听上去和Sleep()差不多,但PreJudge()的指定的时间指的是距离下一波僵尸刷新前的多少时间。参数有两个:第一个是将程序暂停至下一波僵尸刷新前多少时间,以10ms为单位,比如95就是将程序暂停至下一波僵尸刷新前950ms;第二个是下一波僵尸是否是大波,即第10波或者是第20波,是大波的话参数为true,不是的话参数为false。

这个函数是最核心的函数之一,让对轴变得简单方便。比如如果想要实现一个操作“在下一波僵尸(不是大波)刷新前950ms发射一对炮,分别落在 (2, 9) 和 (5, 9)”,可以直接通过下面三行代码来实现:

PreJudge(95, false);
Cannon(2, 9, used_cannons);
Cannon(5, 9, used_cannons);

其中 PreJudge(95, false) 的作用是暂停程序至下一波刷新前950ms。这确实比较绕,如果对编程有一定基础的话会更容易理解一些。

如果我想要在下一波刷新前950ms发一对炮,但下一波是大波的话,可以这样来实现:

PreJudge(95, true);
Cannon(2, 9, used_cannons);
Cannon(5, 9, used_cannons);

可以看出,这段代码和上面一段唯一的区别就是PreJudge()的第二个参数。

值得注意的是,PreJudge()设定的时间只能是下一波刷新前多少时间而不能是刷新后多少时间。如果我想要完成一个操作“在下一波僵尸(非大波)刷新后1650ms发射一对炮,分别落在 (2, 9) 和 (5, 9)”,可以这样实现(这是非大波的实现方法,大波的实现方法大同小异,可以参考上面的代码):

PreJudge(10, false);  // 第一个参数设定一个比较小的值
Sleep(100 + 1650);  // Sleep(100)之后就是下一波僵尸刚刚刷新,这时候Sleep(1650)就是僵尸刷新后1650ms了
Cannon(2, 9, used_cannons);
Cannon(5, 9, used_cannons);

可以看到这里用PreJudge()和Sleep()的搭配来制造出了将程序暂停至下一波僵尸刷新后1650ms的效果。PreJudge(10, false) 将程序暂停至了下一波刷新前100ms,而 Sleep(100) 的作用是将程序暂停100ms,也就是下一波刚刚刷新的时候,这时使用 Sleep(1650) 就可以将程序暂停至1650ms后了(Sleep(100 + 1650) 的效果等于使用 Sleep(100) 和 Sleep(1650))。

这时候你可能已经发现了一个问题:既然如此,那为什么不直接用 PreJudge(0, false) 和 Sleep(1650),反而还要再加一个100ms呢?这就涉及到了一个非常关键的事情:PreJudge()的第一个参数不能为0!这是由于程序设计的一些缺陷······当然,这个值也不一定非得是100ms,也可以是其他你喜欢的值,不过这个值大了可能会导致误差,小了又可能会陷入死循环之中。个人建议这个值最好在200ms~500ms之间,100ms可能已经有点小了。

总而言之,如果你能把上面讲的全部看懂,那么你也已经掌握了PreJudge()这个函数。不懂也没关系,毕竟这也确实有些难度,只要能看懂上面的例子、明白基础用法就行,有啥不会的可以找我来问,用着用着也许就明白了呢:-D

PressSpace()

这个函数会在后台按一次空格,也就是暂停游戏,可以用于女仆秘籍。需要注意的是,按一次空格只会把游戏暂停,不会暂停键控程序(与Sleep()相反),因此一般会搭配Sleep()使用。想要解除暂停的话再按一次空格(即再调用一次PressSpace()函数)就行。如果我想要完成操作“暂停游戏,并在3秒后解除暂停”,可以用以下代码来实现:

PressSpace();
Sleep(3000);
PressSpace();
Pnt()

这个函数会点击一次场上指定的格子,和Card()搭配可以做出种植物、铲植物的操作。由于设计上的一些原因,Pnt()的参数用的是一个std::pair而不是两个数,因此在使用Pnt()时要遵循一些特殊的格式。举个栗子,我想点击场上第2行、第9列的格子,那我可以这样写代码:

Pnt(std::make_pair(2, 9));

想要点击其他格子的话,将数字替换为自己想要的就行。如果我们想要将第三个选择的植物种植到第2行、第9列的话,我们可以这样写代码:

Card(3);
Pnt(std::make_pair(2.0, 9.0));

可以注意到上面的2.0和9.0都是小数,这是因为在发炮时炮弹的落点不一定是要在格子的正中央。如果想将落点进行一些偏移的话,可以通过小数来进行实现。

QuitController()

退出键控工具。这个我们不用管,在键控模板里已经有了。

RecoverCannon()

功能与Cannon()类似,参数也一样。与Cannon()不同的是,RecoverCannon()在场上没有可用的玉米加农炮时,不会直接跳过发炮的指令,而是会一直暂停程序直到有玉米加农炮冷却完毕后进行发炮。简单地讲,在场上所有炮都在冷却时,调用Cannon()会无事发生并直接执行脚本的下一条指令,调用RecoverCannon()则会一直等到有炮冷却好了之后进行发炮操作,发完炮了才会继续执行脚本的下一条指令。

SafeClick()

安全点击,用来防止一些意外情况,平常用不到。

StartIceFiller()

启动自动补冰功能,在后文会有详细描述。

WakeIce()

点冰,该函数没有参数。在使用了该函数后,会从指定的冰位(即第一个修改区中的“kIceList”)中顺次挑选一个,并唤醒该冰位的寒冰菇。支持咖啡豆或模仿咖啡豆,但不支持同时使用,只能选一个用(好像有点鸡肋哈,以后或许会调整一下),可以在InitController()中进行设置。

请注意,如果自行点冰的话WakeIce()并不会记录,也就是说自行点冰后再调用WakeIce()可能会在你已经点过的冰位再点一遍,实际应用时最好从WakeIce()和手动点冰里选一个。

如果开启了自动补冰功能的话,只能用WakeIce()进行点冰,手动点冰并不会触发补冰;相应地,使用WakeIce()进行点冰后必然会触发自动补冰。

进军修改区

在看完脚本模板之后,不知道大家有没有发现一些东西,比如“以下为修改区”什么的,这是标出了在模板中我们可以修改的地方。接下来,我将以经典十二炮为例为大家讲解如何写出一份键控脚本。

请大家先用在上一篇指南中安装的代码编辑器打开pvz_controller_by_cpp.cc,以下所有的修改都默认是在该文件中进行的。另外,在C++中每一行语句的结尾都必须加上英文半角的分号(一些if语句和带花括号的语句除外),千万不要忘了!

经典十二炮阵型展示和节奏简介

为不太了解经典十二炮(以下简称“十二炮”,阵型如下图所示)的童鞋们简单讲解一下,十二炮的主体节奏其实就是每小波僵尸刷新前950ms(大波是550ms)发射一对炮(即传说中的P6节奏),就是这么简单。在此基础上,第9波、第19波和第20波的结尾要额外发射两对炮用来收尾。另外,十二炮如果用P6节奏的话需要处理中场延迟的问题。第20波可以直接炮消延迟,但第10波不行,所以在第10波可以用灰烬植物来补加运算量,我用的是樱桃炸弹。最后,在第20波刷新前150ms要发射一发炮来消珊瑚。

展示经典十二炮的阵型
第一个修改区(玉米加农炮的位置)

我们将目光转回程序上。首先,我们找到第一个修改区,在这个修改区我们需要先将阵中玉米加农炮的位置写入程序中。玉米加农炮占了两个格子,但每个加农炮我们只需要输入一个格子的位置,选前轮还是后轮的格子都没关系,我这里是全部选的后轮的格子位置。从图中可以看出经典十二炮阵在第1行的第5列,第2行的第5列,第3行的第1列、第3列、第5列、第7列,第4行的第1列、第3列、第5列、第7列,第5行的第5列和第6行的第5列各有一门玉米加农炮。

接下来,我们将这些位置信息换一个形式。比如第1行第5列,我们可以将其记为 {1, 5},第2行第5列记为 {2, 5},其他位置以此类推。然后,我们找到修改区里“int kCannonList[kMaxCannonNum + 5][5] = { 0 };”一行,将其中的“0”删去,替换为我们转换过形式的位置信息,位置与位置之间以英文的半角逗号隔开,逗号和大括号之间可以有空格或者换行。

上面的步骤完成后,我们还需要将阵中存冰位的位置信息填入程序中。找到“int kIceList[kMaxIceNum + 5][5] = { 0 };”一行,将其中的“0”删去,仿照上面的操作将存冰位的位置信息进行转化,并填入原来“0”的位置。如果阵中没有存冰位的话,保持“0”不变就行。

修改前和修改后的代码对比如下图,这里为了美观用了空格和回车调整了格式。

展示修改前的第一个修改区
修改前
展示修改后的第一个修改区
修改后

为了方便大家理解,我再放一份经典八炮的键控脚本在这一部分的代码(注:下图中的代码有点旧了,并没有包含“int kIceList[kMaxIceNum + 5][5] = { 0 };”,大家能理解意思就行)。

展示经典八炮键控脚本第一个修改区
经典八炮

当然,大家自己写的时候不加空格和回车,把所有东西都堆在一行也是可以的,就像下面这样,只是有点丑而已。

展示没有调整格式的第一个修改区
不用空格和回车调整格式
第二个修改区(地图类型和玉米加农炮的数量)

接下来,我们找到第二个修改区,如下图所示:

展示修改前的第二个修改区

第一步,我们先找到“’\0’”,将其中的“\0”删去(注意不要把单引号删了,只删除“\0”),替换为你要打的阵所在的场景对应的英文名称的第一个英文字母,要大写的。比如我们这里要打的是经典十二炮,所在场景是泳池,泳池的英文是Pool,也就是说我们要将“\0”替换为“P”,如下图所示。

展示修改场景后的第二个修改区
将“”替换为“P”

再比如我们要打前置八炮,该阵所对应的场景是黑夜,黑夜的英文是Night,这时候我们就要将“\0”替换为“N”了。注意,一定要替换为大写英文字母!

各个场景对应的英文名称和应该填入的英文字母如下所示:

场景名称对应英文名称应填入的英文字母
白天/白昼DayD
黑夜NightN
泳池PoolP
浓雾/雾夜FogF
屋顶RoofR
月夜MoonM

第二步,我们找到“’\0’”后面的逗号的后面的那个“0”,将其替换为你要打的阵中玉米加农炮的数量。比如我们要打经典十二炮,该阵中一共有十二门玉米加农炮,我们就将其替换为12,如下图。同理,如果我们要打其他的阵,比如经典八炮,就需要将这个数字替换为8。

展示修改炮数后的第二个修改区
将“0”替换为阵中玉米加农炮的数量

正常情况下,我们填到这里就可以结束了,剩下的保持原状就行。不过如果后面你要用到WakeIce()的话,下面就得好好填了。

第三步,我们找到下面一行的第一个“0”(即“kIceList”前面的那个),将其替换为阵中存冰位的数量。我们要打经典十二炮,而阵中一个存冰位也没有,我们让其保持0即可。

第四步,我们找到“kIceList”后面的四个参数。第一个参数是点冰时是否要用模仿咖啡豆,用普通咖啡豆填false,用模仿咖啡豆填true。请注意,选择了其中一个之后就不能用另外一个了(单指WakeIce()用不了另外一个了,手动点冰请随意,但不建议WakeIce()和手动点冰同时使用)。第二个参数是咖啡豆是第几张选择的植物卡,如果上一个参数填的是true那么这里就要填模仿咖啡豆是第几张选的。

如果只是想通过WakeIce()点冰的话,那么到这里就可以结束了。如果想要启用自动补冰功能的话,请继续接下来的修改。第三个参数是寒冰菇是第几张选的植物卡,第四个参数是模仿寒冰菇是第几张选的植物卡。

请注意,虽然自动补冰功能支持同时使用寒冰菇和模仿寒冰菇,也支持单独使用普通寒冰菇,但不支持单独使用模仿寒冰菇。如果想要单独使用模仿寒冰菇进行自动补冰,请转阅下文的“【进阶】自动补冰功能介绍(V1.0.2版本更新)”。

这里我们要打的阵是经典十二炮,既不用点冰也不用自动补冰,上面的五个参数保持不变就行。这样子,我们就完成了在这个修改区中全部需要修改的东西。

第四个修改区(选卡,若关闭了自动选卡可跳过该步骤)

我们先跳过第三个修改区,来看第四个修改区。在这个修改区,我们要配置自动选卡该选哪些植物卡。我们给每张植物卡都赋予三个属性:它在选卡界面的第几行、第几列和它是否是模仿者。根据这三个属性,我们可以用三个数字把一张植物卡给表示出来:第一个数字是它在第几行;第二个数字是它在第几列;第三个数字代表它是否是模仿者,0代表不是,1代表是。

比如寒冰菇这张卡,它的位置是在选卡界面的第二行、第七列,那么我们就可以用这三个数字来代表这张卡:2, 7, 0。如果我们想选模仿者南瓜套,它在第四行、第七列,我们可以这样表示这张卡:4, 7, 1。

我们先把自己想要选的卡按照上面的方式用数字表示出来,接着找到修改区代码末尾处的大括号里的“0”,并将其替换为我们处理后的数字,数字之间以英文的半角逗号隔开,中间可以有空格或回车。假设我们这里要依次选择寒冰菇、模仿寒冰菇、咖啡豆、毁灭菇、荷叶、窝瓜、樱桃炸弹、三叶草、南瓜头和小喷菇,修改前和修改后的对比如下所示:

展示修改前的第四个修改区
修改前
展示修改后并调整了格式的第四个修改区
修改后(已调整格式)
展示修改后但未调整格式的第四个修改区
修改后(未调整格式)

通过空格和回车来调整格式这事吧,其实就是为了好看,大家嫌麻烦可以不整,像第三张图这种也是可以的。

第三个修改区(主体节奏)

终于,我们到了最重要的一个修改区。在这个修改区,我们需要将打阵的流程(即“节奏”)全部通过代码来描述出来。为了方便起见,我有时会用语言描述来代替真实的代码(即所谓“伪代码”,但格式更口语化)。

拿经典十二炮举例,首先我们需要将它的主要节奏描述出来。经典十二炮的主要节奏是“每波前950ms发射一对炮”,而每轮一共有二十波,也就是将发炮操作重复二十遍就行,就像下面这样。

等待至波次刷新前950ms
发射一发炮到 (2, 9)
发射一发跑到 (5, 9)

等待至波次刷新前950ms
发射一发炮到 (2, 9)
发射一发跑到 (5, 9)

等待至波次刷新前950ms
发射一发炮到 (2, 9)
发射一发跑到 (5, 9)

············

当然,你想这么做也不是不行,不过这样费时又费力,有什么错误又要一个一个改,简直是噩梦。通过观察上面的代码描述,我们发现了一个规律,就是每一波的主要节奏都是高度相似的(废话,本来就是一样的)。那么有没有什么办法将它们精简一下呢?有的。现在,让我们请出我们的救星,for循环!

for (int wave(1); wave < 21; ++wave) {
  等待至波次刷新前950ms
  发射一发炮到 (2, 9)
  发射一发跑到 (5, 9)
}

可以看到,我们用for循环精简后的代码和之前相比不知道短了多少,而且非常简洁明了。for循环每次循环前会先检测wave是否小于21(wave的初始值是1),如果通过了会继续执行花括号内的代码,执行结束后将wave加一,接着又回去检测wave是否小于21,通过则重复上述的操作,未通过则结束循环。简而言之,在这份代码中花括号内的语句会被执行20次。

在PCC的脚本模板中的第三个修改区本身就是在一个for循环中,也就是说我们不需要自己写for循环了(^-^)V

接着我们将上面的代码描述转化为真正的代码,接着将其写入修改区内。由于修改区在for循环里,所以我们只要写一份就足够了。修改后的结果如下:

展示第一步完成后的第三个修改区
因为在for循环里,所以这三条语句不用重复写20次

但这其实还没有完,因为如果这么写的话每一波都会执行如上的代码,但这份代码只能适用于普通波次,对于大波来说会出问题。因此,我们需要引入if语句来进行“区别对待”。不清楚if语句如何使用的,个人建议可以去网上搜一搜C++语言的基础教程,不需要太长时间几分钟就够了,因为我嘴笨也不太会教这个[捂脸]

if (10 == wave || 20 == wave) {
  PreJudge(55, true);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
} else {
  PreJudge(95, false);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
}

以上便是用if语句调整后的代码。第一行中的“||”代表的是逻辑判断中的“或”,类似的还有“&&”代表“与”。上述代码的意思是如果wave等于10或wave等于20,那么执行后面花括号内的代码,否则执行else后面花括号内的代码。可以发现两部分代码中只有PreJudge一行代码是有差异的,但我却将所有的代码都分割开了,看似有大量重复,但这也是因为后面大波的代码和普通波的代码会有很大的差别,这样子做反而更方便一点。我也推荐大家这么做,用一个if语句对修改区进行分割。

但事实上这样还是不够的。如果执行上述代码的话,会发现场上所有玉米加农炮会在一瞬间齐射。原理有些难解释,只要知道这是电脑执行太快的锅就行了,只需要在每一波的最后加上一个一秒的延迟就行了(一秒不够就两秒,以此类推)。这只是键控程序的延迟,对打阵的节奏不会造成影响。修改后的代码如下所示:

if (10 == wave || 20 == wave) {
  PreJudge(55, true);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
  Sleep(1000);
} else {
  PreJudge(95, false);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
  Sleep(1000);
}

接着,我们要加上在第9波、第19波和第20波收尾的两对炮。第20波和其他两波的情况有些不同,所以我们要分开处理。第9波和第19波收尾时全场炮都在冷却中,没有可用的炮,因此我们需要使用RecoverCannon()来发炮。同时这两波只有收尾部分有所差异,其他都与其他普通波相同,所以在普通波的代码后加了一个if语句。修改后的代码如下:

if (10 == wave || 20 == wave) {
  PreJudge(55, true);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
  Sleep(1000);
} else {
  PreJudge(95, false);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  if (9 == wave || 19 == wave) {
    Sleep(4000);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
  } else {
    Sleep(1000);
  }
}

加了一个 Sleep(4000) 是为了防止发炮发太早了,同样地有了Sleep(4000) 就不需要 Sleeep(1000) 了,就把它放到else里了。

第20波收尾时场上至少有四门可用的玉米加农炮,因此我们可以直接用Cannon()来发炮。Cannon()的性能比RecoverCannon()好些,所以能用Cannon()就优先用Cannon()吧。这时候我们就需要将第10波和第20波分开了,将发炮语句直接写在第20波里面就行。修改后的代码如下所示:

if (10 == wave) {
  PreJudge(55, true);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
  Sleep(1000);
} else if (20 == wave) {
  PreJudge(55, true);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  Sleep(4000);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
} else {
  PreJudge(95, false);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  if (9 == wave || 19 == wave) {
    Sleep(4000);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
  } else {
    Sleep(1000);
  }
}

紧接着,我们需要处理第10波的中场延迟,我自己的办法是用樱桃炸弹,放置樱桃炸弹的时机应卡在第10波刷新后2180ms时。这里假设我们的选卡顺序与上一小节中的例子相同,即樱桃炸弹是第7个选的。修改后的代码如下所示:

if (10 == wave) {
  PreJudge(55, true);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  Sleep(550 + 2180);
  Card(7);
  Pnt(std::make_pair(2., 9.));
} else if (20 == wave) {
  PreJudge(55, true);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  Sleep(4000);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
} else {
  PreJudge(95, false);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  if (9 == wave || 19 == wave) {
    Sleep(4000);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
  } else {
    Sleep(1000);
  }
}

由于节奏的要求是在刷新后2180ms放樱桃,前面有一句 PreJudge(55, true),所以需要先 Sleep(550) 将程序暂停至波次刷新时,然后再 Sleep(2180) 将程序暂停至刷新后2180ms。

在最后,我们需要加入第20波开头的炮消珊瑚。炮消珊瑚的操作是在波次刷新前1500ms发一发炮到 (4, 7),修改后的代码如下所示:

if (10 == wave) {
  PreJudge(55, true);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  Sleep(550 + 2180);
  Card(7);
  Pnt(std::make_pair(2., 9.));
} else if (20 == wave) {
  PreJudge(150, true);
  Cannon(4, 7, used_cannons);

  Sleep(950);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  Sleep(4000);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
} else {
  PreJudge(95, false);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  if (9 == wave || 19 == wave) {
    Sleep(4000);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
  } else {
    Sleep(1000);
  }
}

这时候可能就会有人疑惑了:为什么你不用 PreJudge(55, true) 了,反而改成了 Sleep(950) 呢?事实上,由于程序设计上的一些缺陷,每一波最好只用一次PreJudge(),剩下的轴可以用Sleep()来调。PreJudge(150, true) 会将程序暂停至波次刷新前1500ms,这时候 Sleep(950) 会将程序再暂停950ms,相当于将程序暂停至波次刷新前550ms,效果与 PreJudge(55, true) 等同。

将刚才写出的代码填入修改区吧,现在我们终于写出了第一份键控脚本。整个pvz_controller_by_cpp.cc的内容应该是像下面这样的:

#include <cassert>
#include <cstdio>
#include <cstring>

#include <array>
#include <thread>
#include <queue>
#include <utility>

#include <Windows.h>

#include "pvz_controller.h"

constexpr int kMaxCannonNum(24);
constexpr int kMaxIceNum(54);

// 以下为修改区(玉米加农炮的位置和存冰位的位置)

int kCannonList[kMaxCannonNum + 5][5] = { {1, 5}, {2, 5}, {3, 1}, {3, 3},
                                          {3, 5}, {3, 7}, {4, 1}, {4, 3},
                                          {4, 5}, {4, 7}, {5, 5}, {6, 5} };
int kIceList[kMaxIceNum + 5][5] = { 0 };

void ChoosingCards(void);

// 以上为修改区

int main(void) {
  // 记录用过的炮,并同时记录发炮时的时间戳
  std::queue<std::pair<time_t, int> >* used_cannons;

  // 以下为修改区(地图类型、玉米加农炮的数量、存冰位的数量和各个植物卡的位次)
  if (InitController('P', 12, kCannonList, &used_cannons,
                     0, kIceList, false, 0, 0, 0)) {
  // 以上为修改区
    // 如果想要关闭自动补冰可以在下面一行前面加上“//”
    StartIceFiller();

    // 如果想要手动选卡可以在下面一行前面加上“//”
    ChoosingCards();

    std::puts("\n======= 战斗开始 =======");

    for (int wave(1); wave < 21; ++wave) {
      printf_s("\n正在准备处理第 %d 波的敌人\n", wave);

      // 以下为修改区(主体节奏)

if (10 == wave) {
  PreJudge(55, true);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  Sleep(550 + 2180);
  Card(7);
  Pnt(std::make_pair(2., 9.));
} else if (20 == wave) {
  PreJudge(150, true);
  Cannon(4, 7, used_cannons);

  Sleep(950);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  Sleep(4000);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);
} else {
  PreJudge(95, false);
  Cannon(2, 9, used_cannons);
  Cannon(5, 9, used_cannons);

  if (9 == wave || 19 == wave) {
    Sleep(4000);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
    RecoverCannon(2, 9, used_cannons);
    RecoverCannon(5, 9, used_cannons);
  } else {
    Sleep(1000);
  }
}

      // 以上为修改区
    }

    std::puts("\n======= 战斗结束 =======");
    QuitController(&used_cannons);
  }

  return 0;
}

void ChoosingCards(void) {
  // 以下为修改区(要选的植物卡)
  std::array<std::array<int, 3>, 10> cards{
    2, 7, 0,
    2, 7, 1,
    5, 4, 0,
    2, 8, 0,
    3, 1, 0,
    3, 2, 0,
    1, 3, 0,
    4, 4, 0,
    4, 7, 0,
    2, 1, 0
  };
  // 以上为修改区

  std::puts("\n======= 开始选卡 =======");

  for (auto& iter : cards) {
    ChooseCard(iter.at(0), iter.at(1), iter.at(2));
    Sleep(100);
  }

  std::puts("======= 选卡结束 =======");

  Sleep(200);
  LetsRock();
}

恭喜你,你已经完成了所有步骤中最难的一步。在成功的大道上昂首挺胸地前进吧!

【进阶】开始多线程之旅

先占个坑,以后再来填~

【进阶】自动补冰功能介绍(V1.0.2版本更新)

自动补冰功能是V1.0.2版本的新增功能,默认处于关闭状态。在开启自动补冰功能后,每一次点完冰之后会自动把冰补上。如果冰此时正在CD中,则会等待至冰CD好后把冰补上。注意,这里的“等待”并不会影响程序的进行,不会使程序的运行暂停。如果有多个冰位需要补冰,则会按照点冰的顺序进行补冰。

请注意,如果想要自动补冰的话,请使用WakeIce()函数点冰。如果是自己点冰(即Card() + Pnt())的话,则不会触发自动补冰。

如何开启自动补冰功能

首先,我们找到如下的两行代码,并按照提示操作即可。开启前和开启后的代码对比如下图:

展示自动补冰开启前的相关代码
自动补冰开启前
展示自动补冰开启后的相关代码
自动补冰开启后
如何单独用模仿寒冰菇补冰

上文有提到,PCC的自动补冰功能支持单独使用普通寒冰菇,也支持同时使用寒冰菇和模仿寒冰菇,但不支持单独使用模仿寒冰菇。如果想要单独使用模仿寒冰菇的话,也可以,只要将模仿寒冰菇当做普通寒冰菇来写代码就行,比如在InitController()中将模仿寒冰菇的卡片次序填至第九个参数(本来应该填普通寒冰菇卡片次序的),并将第十个参数填“0”。

不过,单独用模仿寒冰菇补冰这事也没啥意义,可能就是为了好看吧hhh

生成键控程序

在完成了“编写脚本”一步后,不管是用的示例脚本还是自己编写的脚本,在PCC的安装目录下都应该有了一份已经编写好的pvz_controller_by_cpp.cc文件。在这一步中,我们要将键控脚本转变为可以运行的程序。尽管这一步操作起来非常简单(只需要运行一个.bat文件),但却是最容易出错的一步。不光是因为什么环境啊什么兼容性啊的,再就是上一步中编写键控脚本时如果出现了一些失误,都会在这一步中体现出来。

首先,我们在PCC的安装目录中找到文件“点我生成键控程序.bat”,并双击运行。经过短暂的等待后,该目录下应该会生成一个文件“PvZ_Controller_by_CPP.exe”,这就是我们最终得到的键控程序,如下图。

展示生成键控程序的安装目录

如果成功生成了这个文件,那么恭喜你完成了这最容易出错的一步。如果在运行完“点我生成键控程序.bat”却无事发生或者弹出了错误提示的话,很遗憾在之前的步骤中肯定出了什么问题。不过不用灰心,我也为大家提供了一些解决方案,来帮助大家排除错误这样子~

出错了该怎么办

请根据以下错误类型对号入座~

注:这一部分会保持更新。

找不到ucrtbased.dll

如果你此时出现了如下图所示的错误,请根据下面的步骤来解决,可能会用到管理员权限。

展示找不到ucrtbased.dll的错误信息

首先,我们在PCC的安装目录下找到“dependences”文件夹并打开,在其中找到ucrtbased.dll文件。接着,请按个人情况继续以下操作。

  • 如果你的电脑是64位的,请将ucrtbased.dll文件拷贝至 C:\Windows\SysWOW64 目录下(假设C盘是你的系统安装盘),该步骤可能会用到管理员权限
  • 如果你的电脑是32位的,请将ucrtbased.dll文件拷贝至 C:\Windows\System32 目录下(假设C盘是你的系统安装盘),该步骤可能会用到管理员权限

完成以上操作后,问题应该已经解决。如果依然出现同样的错误,可以通过贴吧或者博客评论来告诉我,我会好好认真解答的~

其他错误

如果你的错误不是以上错误的任何一种,那么你可能需要联系人工客服,没错就是我。大家可以通过贴吧或者博客的评论或私信来联系到我。

不过,如果你没有带图片或者视频来找我,我是不会回你的哦。这里的图片指的是错误信息的图片,可不是······当然,你要是带了······那我也是欢迎的:-D

可能你就要问了,我这按你的步骤操作完完啥也没发生,咋截错误信息的图啊?不用担心,我这就来教你怎么办。

首先,我们依次打开“开始”菜单、Windows 系统、命令提示符(或者直接按快捷键Win+R,并在弹出的窗口里输入“cmd”然后回车),打开后会弹出一个黑色的窗口,如下图:

展示命令提示符的窗口

最后一行的第一个字母告诉了我们现在在哪个盘,看看和你安装PCC的目录在不在一个盘。如果不在一个盘的话,在黑窗口里输入PCC安装目录所在的盘对应的字母,后面加个英文的半角冒号,然后回车。在这个例子中,黑窗口现在在C盘,而我的PCC安装在了D盘里,所以我要在黑窗口里输入“D:”并回车,如下图。如果二者在一个盘里的话,那么就什么都不用管,直接进入下一步就行。

演示截取错误信息的步骤(第一步)

接着输入“cd”两个字母,并跟上一个空格,不要回车!然后找到你PCC的安装目录,我这里是 D:\Softwares\PvZ_Controller_by_CPP_V1.0,把这个目录地址输入在你刚才输入的东西的后面然后回车,如下图。

演示截取错误信息的步骤(第二步)

最后,输入“.\点我生成键控程序.bat”并回车(不要双引号)。如果不出所料的话,应该会输出一堆错误信息(如下图)。把这些错误信息全部截下来就行,最好截得全一点,找我反馈的时候记得要带上截的错误信息的图哦,下面这张就是一个不错的例子。

展示错误信息的截图

由于个人水平的原因,不是所有的错误我都可以解决的,也请大家谅解。如果遇到了这种情况,可以在贴吧发帖问问,让各路大佬都来帮忙看看。

修改游戏

请注意,这里的修改游戏指的并不是作弊。我们只是用修改器修改了该局内的植物阵型、阳光数、出怪等,方便我们打阵,这也是完全符合规矩的。当然,如果你想要自己布阵的话也行,可以直接跳过这步去看下一步了。

首先我们打开游戏,并进入对应的无尽关卡。我这里以经典十二炮为例,所以就直接进了泳池无尽。注意要新开一局哦!进去之后等到了选卡界面,再继续以下的操作。

打开在上一篇指南中下载的修改器,进行以下修改:

  • 修改轮数至任意大于等于124轮的轮数(为什么不是125轮后面会说,我在这个例子里选的是1110轮)
  • 布置你键控脚本所对应的阵型(这里是经典十二炮)

以上修改是必须的,我在下图中用红笔圈了出来。当然,你可能还会有修改阳光、删除小推车、导入阵型代码等需求,这些不必须但又比较常用比较重要的功能我在下图中用蓝笔圈了出来。

如果运行修改器时出现错误了的话,可以先关闭修改器,然后以管理员身份重新打开修改器试试(右键修改器程序,在弹出的菜单中点击“以管理员身份运行”)。如果还是不行,把修改器界面截个图然后来找我,没有图片或者视频的反馈我是不接受的!

演示如何用修改器修改游戏(第一步)
演示如何用修改器修改游戏(第二步)
演示如何用修改器修改游戏(第三步)

接着,随便选十个植物卡然后直接进入游戏,什么都不用管。进去之后先暂停游戏,然后打开修改器,在“资源”页找到“直接过关”并点击,回到游戏应该可以发现直接进入了下一轮。没错,这就是上文为什么写的是124轮而不是125轮的原因了,因为我们真正要进入的不是修改器里修改的轮数而是修改器轮数的下一轮啊。至于为什么要这么做,限于篇幅原因就不展开讨论了。

现在我们又回到了选卡界面。在这里,我们要进行以下修改:

  • 修改出怪方式为全难度极限出怪(这是键控炮阵的出怪要求,如果是其他类型的阵请按对应的方式出怪)
演示如何用修改器修改游戏(第四步)

现在,我们完成了所有的修改,可以进入下一步骤啦~

一起摇滚吧!

让我们在PCC的安装目录里找到在“生成键控程序”一步里生成的键控程序“PvZ_Controller_by_CPP.exe”。如果关闭了自动选卡的话,在游戏中需要先选卡进入游戏再运行键控程序,反之则需要将游戏停留在选卡界面,以下以开启了自动选卡的情况为例子。

在运行了键控程序之后就不要再拖动游戏窗口或者乱点鼠标了,切记!

双击运行键控程序,在窗口出现后5秒内切换至游戏界面,如果没来得及切换的话你会发现你的鼠标会像抽风了一样到处乱点。不过慢了也没关系,等鼠标抽完风之后自己手动选卡再进入游戏也是可以的。

需要注意的是,自动选卡有不小的几率会失误,如果出现了选错卡的情况可以手动重选并进入游戏,当然重新运行键控程序也是可以的。

如果一切正常的话,此时你会发现键控程序已经按照你设定的程序帮你选好了卡并自动进入了游戏。此时一切工作已经完成,好好享受你的劳动成果吧!

此阶段出了什么错的话,个人建议是录一个视频,从运行键控程序开始一直录到出错为止,把键控程序的窗口和游戏窗口一起录上,然后再联系我。当然截图也不是不行,就是截得太多可能会有点麻烦。如果没有图片或者视频的话我是不会听你说话的w

后记

PCC的教程终于写完了,也算是挺有成就感的。感谢贴吧大佬的珍贵经验,本程序的开发思路来源于no_doudle大佬的python键控工具,示例脚本是仿着向量大佬的AVZ的示例脚本一步一步改的(原谅我实在不会打阵)。

V1.0.1和V1.0.2两个版本应该是自发布以来最重要的两个版本了(请原谅我不会取版本号,原本应该是V1.1和V1.2的hhh)。V1.0.1更新了后台点击功能,解决了PCC的精度问题;V1.0.2更新了点冰+自动补冰功能,有效降低了编写键控脚本的复杂程度,该版本同时更新了二十四炮的示例脚本(是我自己看视频摸出来的节奏哦,不是仿着AVZ的示例脚本改的)。从这两个版本起,PCC也终于能上得了台面了[泪目]

最后还是要感谢植吧的各位。植吧在我成长的路上给了很多的帮助,我也是想回报植吧一点什么才决定做这个东西的,虽然我也知道做的很烂······不管怎样,祝愿我和植吧都能越来越好吧(抱歉我实在想不出什么文艺一点的词233)。

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

返回顶部