解释一下AvZ的IQ/NIQ
仅为群友而写。
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,你可以自行查看感受一下两者的区别喔。