解释一下AvZ的IQ/NIQ
Crescebdo
编辑于 2022年11月26日 15:22
收录于文集
共15篇

仅为群友而写。

AvZ里,有一个神奇的东西叫“InsertOperation”。

对萌新而言,是完全用不上这个东西的。以经典12脚本为例:

无非就是一直SetTime,然后pao_operator.pao,再配合C++自带的循环语法就够了。

不用InsertOperation,你可以满足99%的键控需求,至少把炮阵一百选或者我空间里绝大多数视频打一遍毫无问题。

那么这个InsertOperation又是何方神圣呢?

举个🌰,假设你知道avz_more(一个AvZ扩展)里提供了读取当前阳光的函数:

现在你想在wave11刷新时,调用这个函数,输出当前阳光。

萌新可能会这么写:

然而实际上,这段代码会输出本次选卡开始前的阳光数,而非实时数值。

这是不是很令人迷惑?毕竟,输出语句的确是wave11刷新时执行的。它怎么会输出一个旧的值呢?

要理解这个事情,你只需要记住一句话:

AvZ脚本里所有代码在进入生存无尽的一瞬间就运行完了。

AvZ脚本里所有代码在进入生存无尽的一瞬间就运行完了。

AvZ脚本里所有代码在进入生存无尽的一瞬间就运行完了。

这件事非常重要!不理解的话请换个姿势多读几遍。

上面这个例子里:

GetSun()是代码,没错吧?

前面那句话说,AvZ的所有代码都是在进入生存无尽的一瞬间运行的。

因此,GetSun()是进入生存无尽的一瞬间运行的。

由于你点进游戏生存无尽的一瞬间是本次选卡开始前,所以GetSun()的值当然就是选卡开始前的阳光数(比如8000),而非实时数值咯。

如果你在脚本里加一句 SetErrorMode(CONSOLE),当AvZ注入完毕后,点开游戏时会跳一个黑色调试窗口。实际上跳出这个黑色窗口的瞬间,所有代码就都运行完毕了。

等等……

你逗我呢?

如果所有代码都是一开始就运行完了,那AvZ是怎么发炮的?

如图,pao_operator.pao(2, 9)也是代码啊。那它岂不也是刚点进游戏就运行了?可是实际上它明明是在我们指定的时间点才会发炮。

原因很简单…… 

运行代码,不等于现在就执行操作,也可能是过一会执行操作。

打个比方,“运行”就像是定罪,但定罪后不一定立即执行,也可以是缓刑啊((

这一段代码,的确是一进入游戏就运行的。但由于SetTime的作用,它的实际含义为:

  • 我决定,在未来的一个时间点(wave1刷新前95cs),发射一门炮

请记住:这个决定,是进入游戏的瞬间就完成了的。它只是过一段时间才执行,看似有“延迟”,像是“wave1快要刷新了才发炮”,但其实发炮这件事早就定了,你把玉米炮全挖了它一样会执行(顺便报个错),不存在任何取消的方式。

正题——InsertOperation

理解了以上内容后,就很容易理解InsertOperation的意义了。

它的作用是:将我内部的所有代码,推迟到最近一次SetTime设定的时间点执行。

这个最近一次SetTime,是根据代码运行的顺序来的,也就是进入游戏的那一瞬间发生的事。

AvZ里许多函数都封装了InsertOperation。比如,pao_operator.pao()实际内容是这样的:

有没有看到那个熟悉的InsertOperation?

它的作用,就是让红框里所有的代码,都等到最近一次SetTime设定的时间点再执行,而非立刻执行。

在你眼里,代码是这样的:

在电脑眼里,它其实是这样的:

以上两段完全等价。pao()函数的唯一作用,就是把这一大段东西(InsertOperation+一堆代码)缩略一下。

其实pao也好,Card也好,各种常用键控函数底层都会用到InsertOperation。这也是理所当然的——要不然所有操作都一上来就执行了,还玩毛啊。

现在你知道了,为什么你可以一个SetTime后跟好多操作。

以上三个语句,都会在 (-95, 1) 这个时间点执行。原因就在于,它们底层都用到了InsertOperation,而InsertOperation只看最近一次SetTime。因为这里只SetTime过一次,大家当然就都共用 (-95, 1) 这个时间点咯。

你还知道了,为什么不SetTime直接用pao会翻车。

还是经典12脚本,本来白框里是SetTime(-150, 20),也就是炮消珊瑚时机。

如果强行把这个SetTime去掉,会发生很奇怪的事…… 为什么?

原因在于,pao_operator.pao底层是InsertOperation,而InsertOperation永远会去找最近一次SetTime。在这里,就是上面那个循环里的最后一次SetTime:

这显然不是我们想要的。但很可惜,SetTime就是认这个死理。所以你的炸珊瑚炮会在 (300, 20) 这个谜の时间点发射,水路炮早就被啃光了,而你以为AvZ又出了bug怒锤键盘。

区分IQ和NIQ

所谓“IQ”,就是“In Queue”,指必须要配合SetTime使用的函数。操作在指定时间点进行。缓刑。

所谓“NIQ”,就是“Not In Queue”,指不需要配合SetTime使用的函数。操作立刻马上执行。

所有常规代码都是NIQ。而IQ其实就是NIQ外面套了一层InsertOperation。

换句话说:IQ = InsertOperation + NIQ。

看个例子就懂了。AvZ里许多函数都分IQ和NIQ两个版本(pao并没有)。

我们看Card():

没错,你最喜欢的Card函数,其实就是CardNotInQueue外面套了一层InsertOperation的皮…… 有没有被骗了的感觉。Card这个函数并没有任何实质内容。

ShowError当然也是一回事啦:

总而言之,真正“干活”的代码,都是NIQ。IQ函数只是方便你在SetTime后调用的工具人。

回到刚开始这个例子:

这一段代码为什么不对,现在就很明显了吧?

ShowError是AvZ官方函数,内部套了InsertOperation,所以它可以配合SetTime正确执行。

但是GetSun()只是在读取内存啊亲!它内部才一行啊亲!它当然是立刻执行的啊亲!

解决方法自然就是把它丢进InsertOperation:

以上为正解。over。

你没事吧?没事别乱用IQ

眼尖的你可能发现了:“错误写法”变为正确写法后,ShowError悄悄变成了ShowErrorNotInQueue:

解释这件事并不难。

“错误写法”里,我们试图用【SetTime + 调用IQ函数】的一般套路,理应用ShowError(虽然在GetSun上翻车了)。

正确写法里,所有代码都在InsertOperation内部,而我们知道InsertOperation就是用来包裹NIQ的,所以理应用ShowErrorNotInQueue。

可是有人就不服啊。他说,我就要IQ和NIQ反着用,会发生啥事?

CASE 1:我偏要在SetTime后用NIQ

这显然是瞎折腾。NIQ函数就是一段普通的代码,而普通代码根本不鸟SetTime。它将在进入游戏的一瞬间立刻执行,立刻输出选卡前的阳光。

CASE 2:我偏要在InsertOperation里用IQ

相比之下,这个行为就非常重量级。

首先,AvZ是允许多重InsertOperation嵌套的。以上代码,实际可以转换为:

看上去,它就是犯了个蠢,卖了个萌,精灵球里又套了个精灵球,但好像没啥实际影响?

错!别忘了,InsertOperation永远会找上一个SetTime,这是死理。

对于第一个InsertOperation,它的上一个SetTime是(0, 11),没毛病。

但对于第二个InsertOperation,由于它在第一个InsertOperation内部,它会等到 (0, 11) 也就是wave11刷新这个时间点才执行。

此时,AvZ脚本已运行完毕,因此它实际上会使用脚本里最后一个SetTime设定的时间点

如果把无辜的炮消珊瑚加进来:

图上的白色数字,是每行代码的运行顺序。其中,①、②、③、④是进入游戏的一瞬间运行的;⑤、⑥分别在各自InsertOperation设定的时间点运行。

不难看出,运行⑤时,最近一次SetTime其实是炮消珊瑚的时间点,也就是 (-150, 20)。

对电脑来说,它看到的是:

因而,这句输出语句会一直等到 (-150, 20) 才执行!你怎么等也等不到输出,以为AvZ又出bug了然后怒砸键盘。

这个问题,应该就是萌新乱用InsertOperation时最容易犯的错误了,也是很多人表示“无法理解InsertOperation”的根源。

它有三个解决方法:

1. 你没事吧?没事别乱用IQ

记住一句话:

IQ必须要配合SetTime用。

IQ必须要配合SetTime用。

IQ必须要配合SetTime用。

IQ函数的本质是InsertOperation,而InsertOperation永远会去找上一个SetTime,这就是AvZ的死理。

因此,不用SetTime却用InsertOperation的行为被称作“裸奔”,踩香蕉皮滑到哪里是哪里,会造成一系列费解的bug。

除了InsertOperation,pao、Card等等函数也都可能“裸奔”,因为这些IQ函数在本质上都使用的是InsertOperation。

在这个错误例子中,ShowError就是一个裸奔的IQ函数,很轻易地就翻了车。

2. InsertOperation里先无脑SetNowTime()

SetNowTime()是一个鲜为人知却非常好用的函数。

它的作用是:把当前实际时间传进SetTime。

再看上面那个例子:

加上SetNowTime后,腰也不酸了,腿也不痛了,代码也正确执行了。

我们知道,SetNowTime和ShowError一样,都是在(0, 11)这个时间点才执行。而SetNowTime会忠实地把当前时间点传进SetTime,供后续使用。

这样一来,上面的代码实际上就是先 SetTime(0, 11),然后再ShowError,完美解决“裸奔”问题。

总体而言,SetNowTime的确是个万金油,因为它非常符合人类的自然逻辑。对于IQ函数,它默认让它们在当前时间点执行,符合人类的预期;对于NIQ函数,它没有任何效果,不碍事。

当然,如果某个函数有NIQ版本,那还是建议你直接用NIQ,而非SetNowTime + IQ…… 其实两者没啥大区别,就是后者看着略蠢。。。

3. 用InsertGuard

这个是AvZ官方推荐的方式,效果和上一种类似,但是略难理解。

写出来是这样的:

ig是变量名,取任何名字都行。

它的作用是:在当前大括号括起的段落里,使所有InsertOperation失去原本效果,直接运行。那个false就代表失效,如果是true的话就是让它重新生效。

过多的就不介绍了,如果你觉得难以理解的话,先试试看前两种办法吧。

一些理所当然的补充...

理解了以上内容后,以下几点都是显然易证自然成立的。仅作补充用意。

① 我要在XX时刻判断OO僵尸状态,怎么写?

“判断场上僵尸”,其实就是读内存,一个for循环里来几句if。这些都是普通代码,也就是NIQ代码,因此需要写在InsertOperation里(你总不想一进游戏就判断吧,啥僵尸都没有呢)。

在22年6月之前的AvZ里,写法是这样的:

zombieTotal是僵尸总数,zombieArray是僵尸数组,然后isDisappeared() 和 isDead() 检查僵尸是否存在,最后检查type是否为红眼…… 这些你都可以在 pvzstruct.h 里搜到,可自行搜索。

重申:以上代码需放进InsertOperation里。

别忘了,如果在这里要执行pao啊Card之类的操作,记得SetNowTime或者InsertGuard,不理解的话请重读前面一章。

22年6月版本起,你可以直接用“filter”功能:

AliveFilter自动遍历所有还活着的生物。AliveFilter<Zombie>就是遍历所有“活着”的僵尸。

同样,以上代码需放进InsertOperation里

因为本文的重点是学会使用InsertOperation,所以这里就不更多说明怎么读内存了~ 如果有很多人好奇的话我可能会另写一篇文章详解。

② InsertTimeOperation是什么玩意?

直接上源码:

template什么的看不懂可以无视,你只需要关注里面那个“InsertOperation”。

是的,“InsertTimeOperation”就是InsertOperation的套皮版。它额外接受两个参数(time和wave),然后先 SetTime(time, wave),再正常执行InsertOperation。

说到底,因为SetTime和InsertOperation必定要一起用(不然就裸奔了),所以AvZ干脆再提供了一个函数,让你可以把它们直接写在一起。

这两段代码完全是一回事。

简化了很多吗……?好像也就那样(

需要注意的是,InsertTimeOperation内部自带SetTime(可以再去看眼源码,就在上面),这个SetTime当然也是算在“最近一次SetTime”里的!好处是妈妈再也不担心我忘了SetTime了,坏处是它是隐藏性SetTime,看代码的时候容易忘了还有这回事,总之各有利弊吧~ 我觉得只用InsertOperation也挺好的。

请你不要问:如果SetTime和InsertOperation总要一起用,为啥不能只保留InsertTimeOperation?亲,如果这样的话,不就没法SetTime一次InsertOperation多次了。。

下面这个写法就不成立了:

没人愿意把 -95, 1 写三遍吧(


以上就是所有关于AvZ里IQ/NIQ的事啦~

其实这个事情本身没有那么复杂,但可能是原先的教程有点难懂吧,有些人不太理解,那么希望这篇文章能帮到你。

一个小tip:AvZ目录里inc文件夹是AvZ官方函数的头文件,里面所有In Queue的函数都在注释里标明了,而其它的则是Not In Queue,你可以自行查看感受一下两者的区别喔。