第 1 期 · · Voyager #037
机制札记 #1:关卡出怪是怎样生成的
从第 1 关的三点预算出发,推导 0.17q 出怪的条件概率、预算结算与末波保底,验算平滑选行权重,拆解灵魂行和提前刷新的触发条件,再用种子波表核对。
同一关重开几次,有时来的是路障,有时却是一群普通僵尸。这些差别是怎样产生的?游戏又是在什么时候决定它们从哪一行出现的?
要回答这些问题,需要先了解一波僵尸是怎样排出来的。我们从第 1 关的一个小例子讲起,再逐步加入手动出怪、选行和刷新时机。没有接触过关卡文件的读者也可以从头阅读;已经熟悉点数池的读者,则可以留意后面的特殊情况。文中讨论的是 0.17q 的常规出怪系统,规则依据关卡文件与程序逻辑整理。Boss、控制器和预置实体产生的僵尸,还要分别看它们自己的机制。

先看一个三点预算的例子
假设游戏要给这一波安排僵尸,手里有 3 点可用。可选的只有普通僵尸(下文简称“普僵”)和路障僵尸:普僵需要 1 点,路障需要 3 点。两种僵尸都已解锁,抽取权重也相同。为了方便,下文把这份可用点数称为“预算”,把生成一只所需的点数称为“价格”。
先不要急着计算。这一波最后可能是什么样子?
- 一只路障僵尸。
- 三只普通僵尸。
- 上面两种情况都有可能。
如果认为是第三种,就已经抓住了这个例子的关键。不过,我们还需要弄清楚:既然权重相同,为什么数量会差这么多?
我们分两种情况来看。第一次抽到路障,3 点便一次用完,只能得到这一只。第一次抽到普僵,则还剩 2 点;此时已经买不起路障,接下来的两次便只能继续抽普僵,最后得到三只。

这正是第 1 关 MEDIUM 难度第 4 波会遇到的情况。两种结果并不是谁多花了点数,而是同样的点数被用在了不同的僵尸上。
权重相同,不等于数量相同。 权重影响一次抽取的结果,价格则会影响后面还有谁能参与抽取。接下来我们把这条规则展开。
先分清三个决定时刻
可以把一局游戏分成三层来读。开局构造波表决定池中抽到了什么、抽了几只,并为池生成记录安排初始化行;每波出场读取这一波的记录,处理行冷却、随机行与灵魂行;波间推进持续检查计数器与当前波的剩余统计值,决定什么时候读取下一波。
这三层会相互关联,却回答不同的问题。更换阵容可能影响灵魂选行,清怪速度可能影响下一波的到来时间,但都不能据此推导出“点数池会在战斗中重新挑选僵尸”。同样,初始化模拟给出一只僵尸在第 3 行,也不等于运行时一定不会因行状态而改行。后面的预算、选行与刷新,都可以放回这三个时刻定位。
点数池怎样把预算变成僵尸
每抽一次,都要重新看剩余点数
上例中的可选清单,就是点数池,在关卡文件中写作 ZombiePoints.ZombieList。如果打开这份清单,会看到每个候选条目可以设置下面三个参数:
Points:抽到一次要扣多少点,也就是上文所说的价格,默认 1。Weight:本次抽取时使用的相对权重,默认 100。Waves:从第几波开始允许普通抽取,波次从 1 起计;默认 0,表示第一波便可参与。
游戏准备抽取时,先看哪些条目已经解锁,再看剩余预算买得起哪些。只有通过这两项检查的候选,才按各自的权重参与本次抽取。抽中一只以后,扣去它的价格,再用剩下的点数重新检查一遍。
需要注意,“抽到过”并不会让一个条目退出点数池。只要条件仍然满足,它就可以再被抽到。直到预算用完,或者已经没有可抽的候选,本波的普通抽取才结束。
回到开头的例子,路障与普僵只有在第一次抽取时才是等权重竞争。第一只普僵出现后,路障便因价格太高而退出后续抽取。因此,不能把每次抽取都看成“普僵、路障各占一半”。这里说的退出也只针对当前剩余预算,并不是从整关的点数池中删掉了路障。
权重是条件概率,不是整波配额
设当前剩余预算为 R,当前波为 w。先把“解锁波次不大于 w、价格不高于 R”的条目留下,得到候选集合 A。在正常的非负权重配置下,条目 j 本次被抽中的理论概率是 它的权重除以 A 中所有条目的权重之和。候选集合每次都会重建,因此分母也可能改变。若有效权重总和为 0,就停止普通抽取,即使还有剩余点数。
沿用普僵 1 点、路障 3 点、等权重的例子,只把预算从 3 改成 4。在理想等概率模型中,可以把抽取路径完整展开:
- 先抽路障:概率 1/2,剩 1 点,只能再来一只普僵。
- 先抽普僵,再抽路障:概率 1/2 × 1/2 = 1/4,正好用完。
- 前两次都抽普僵:概率 1/4,剩 2 点,之后只能再来两只普僵。
不考虑先后顺序,最终“一路障加一普僵”的概率是 3/4,“四普僵”的概率是 1/4。路障的期望数量是 0.75,普僵是 1.75,总只数的期望是 2.5。两类等权重,既不代表最终数量各半,也不代表每波各分到一半预算。 3 点预算时出现路障的概率为 1/2,4 点时已升到 3/4:多出的 1 点提供了第二次仍买得起路障的机会。
所以“一整波出现某类型的概率”不能直接写成它的权重占比。整波概率要合并所有能走到该结果的抽取路径,每条路径又会改变余额。原程序用整数随机数对总权重取余来抽取;总权重不能整除随机数取值范围时会有微小的取模误差。上面的分数用于解释理想加权模型,不是对所有种子的精确计数。
剩余点数不一定用得完
再换一份教学点数池:只有价格 3 的路障,预算为 5。抽出一只后还剩 2 点,所有候选都买不起,普通抽取就结束;不会自动补两只普僵,也不会为了花完点数超支买一只路障。未使用的余量不会结转到下一波,下一波重新计算自己的预算。
只有在仍有已解锁、正权重、价格 1 的候选等条件下,才可以期待正整数余额被逐点花完。价格决定开支,权重决定选择机会,解锁决定参与资格;改变其中任何一个参数,都可能改变整波的数量分布。以下手算默认价格为正整数、权重非负,不把异常配置当作正常关卡规则。
每波有多少预算
前面直接给了 3 点预算,但游戏是怎样算出这 3 点的呢?普通波的默认基础预算为 B = i / 3 + 1,其中 i 是从 0 开始的波次编号。也就是说,第 1 波代入 0,第 4 波代入 3;除法产生的小数暂时保留。
基础预算还要乘难度倍率 M,最后才转为整数点数。以第 1 关的 MEDIUM 第 4 波为例:
- 先算基础预算:
3 / 3 + 1 = 2。 - 再乘这一难度的倍率:
2 × 1.5 = 3。 - 结果已经是整数,因此这一波有 3 点可用。
五档难度默认使用 1、1.5、2、2.5、3 倍。作者可以用 Multiplier 写一个共同倍率,也可以写五项数组,逐档设置。如果 ZombiePoints.Points 是长度恰好等于总波数 Waves 的数组,就改用数组各项作为每波的基础预算,之后仍然要乘难度倍率。
这里的 Points 容易混淆:写在池条目中,是一只僵尸的价格;写成 ZombiePoints 下的逐波数组,才是各波的基础预算。阅读文件时,先看它写在哪一层。
点数只是游戏用来安排出怪的预算,与阳光、荣光和僵尸 HP 分开计算。路障价格为 3,并不表示它的耐久是普通僵尸的 3 倍。
最后补充整数转换的细节:程序会先加上很小的 0.000001,再截去小数部分;大于这个微量、却仍不足 1 点的正预算按 1 点处理。一般的手算按上述方法即可。若结果恰好落在临界值附近,还需要考虑单精度计算的影响。
倍率增加,数量为什么会跳变
预算最后才整数化,所以难度倍率的变化不是逐只平滑增加。一个普通波的基础预算若为 2.4,乘 1.5 后是 3.6,得到 3 点;乘 2 后是 4.8,得到 4 点。新增的 1 点除了可能增加一只普僵,也可能让某条价格更高的抽取路径继续有资格参与。
自定义逐波数组也不是最终预算。假设某波数组值为 3、倍率为 1.5,结果仍要转换为 4 点。数组只有长度恰好等于总波数才按这里的逐波覆盖规则使用;不能把一份短数组理解为“先覆盖前几波,后面自动接默认公式”。应分别记录基础预算、倍率后的整数预算、特殊扣点后的余额,避免把三个阶段都叫作同一个“本波点数”。
作者指定的僵尸放在哪里
点数池可以解释随机出现的僵尸,却还不能解释另一种常见安排:作者希望某一波一定有一只路障,而且指定它从第 3 行来。这样的记录可以直接写进顶层 ZombieList,不必等点数池恰好抽中。
于是,我们遇到了两份同名列表。顶层 ZombieList 存放逐波的手动记录,ZombiePoints.ZombieList 存放随机追加的候选。一关可以同时使用两者。下面先把随机追加放在一边,只看手动列表怎样分波。
怎样分波
假设依次写下“普僵、普僵、分波标记、路障、结束标记”,这份列表会得到几波?前两只普僵属于第 1 波,越过分波标记后,路障便属于第 2 波。
关卡文件中的分波标记叫 NULLZOMBIE,结束标记叫 ENDNULLZOMBIE。游戏按顺序读取列表,一条普通记录配置一只僵尸;遇到前者,进入下一波;遇到后者,停止读取。
我们还可以把两个分波标记连在一起。此时它们之间没有任何普通记录,就会留下一个没有手动僵尸的波。下图把这两种写法放在一起:

也就是说,分波标记本身不是一只僵尸,空的手动波也不一定是游戏中的空波。另外,相同类型、相同行连续写了两次,仍然是两只,不能因为内容相同便合成一只。本站波表中的 x2 就是在保留这个数量。
同一份列表如何适应不同难度
假设作者想让刚才那只路障只在 HARD 及以上出现,就可以给它加上 LowDif: 30。这里的 30 来自游戏用于比较难度的数值:EASY = 10、MEDIUM = 20、HARD = 30、IMPOSSIBLE = 40、UNBALANCED = 50。
判断方法很直接:当前难度值大于或等于 LowDif,就保留这条记录;否则跳过。不写 LowDif 时按 0 处理,因此五档都能通过这项检查。
如果把门槛从 30 改成 44,IMPOSSIBLE 还能出现这只僵尸吗?虽然 44 离 40 更近,但它并不会被归到 40 那一档。由于 40 < 44,只有数值为 50 的 UNBALANCED 能通过。

LowDif 比较的是数值,不是就近选择一档难度。 关卡介绍中的 Dif 评分是另一项数据,不参与这里的比较。
怎样重复一段波表
除了逐波向后读,手动列表还可以指定一段重复使用的记录。EndlessWave 用来设置循环起点,也从 0 起计。以第 3 关为例,它的手动列表有 10 波,EndlessWave: 7 指向第 8 波。
读完第 10 波后,就回到第 8 波这段记录。本站按 Waves: 15 展开时,第 11~15 波依次复用第 8、9、10、8、9 波。注意,回去的是手动记录的读取位置,页面上显示的波次仍然向后增加。

这里的 15 只是页面采用的展开范围,不能据此认定循环关在游戏里一定于第 15 波结束。实际何时结束,还要看运行时的结束条件。
再加上三条特殊规则
到这里,我们已经知道手动记录怎样分波,也知道点数池怎样花掉预算。再补上几条规则,就能理解一波僵尸的大致构造顺序了:游戏先放入手动记录,再处理自动旗帜队伍、末波保底和手动记录的点数扣除,最后才用剩余预算进行普通抽取。
这一过程是在进入关卡时逐波完成的。也就是说,点数池的种类、数量和初始化选行,在开局构造波表时就已产生。 如果某波恰好来了难以应付的僵尸,不能由此推断游戏刚刚看过你的阵容,临时抽了一种来克制它。后面介绍的灵魂选行会读取植物状态,但那是另一件事。
大波的预算与旗帜队伍
大波通常比普通波热闹一些。默认预算公式会在每个第 10 波和最后一波作大波处理:先把基础预算乘 2.5,再计算附加普通僵尸的数量 N。N 的算法是把 i × M / 3 截去小数部分,然后将结果限制在 2~8。
把一只旗帜僵尸也算进去,队伍总数便是 C = N + 1。如果放大后的基础预算至少为 1.5 × C,就先扣去 C,然后再乘难度倍率、转成整数。我们可以把这一步理解为给旗帜队伍预留点数。
不过,“预留了点数”与“真的生成队伍”需要分开看。要生成一只旗帜和 N 只普僵,还必须开启 ZombiePoints.Flag: true,而它默认关闭。预算中的预留分支本身不检查 Flag,所以不能看到扣点就认定一定有自动旗帜队伍。
若设置了有效的逐波 Points 数组,则以数组值覆盖基础预算。还有一种容易混淆的情况:作者在手动列表里单独写了一只旗帜。这只旗帜属于手动记录,并不会顺便打开自动队伍。最后回算第 1 关时,我们会把大波的数字完整代入一遍。
大波为什么不能直接按普通波的 2.5 倍算
大波处理包含乘法、条件扣除和截断,并不是把已经算好的普通波整数预算乘 2.5。设普通基础预算为 B,大波放大值为 G = 2.5 × B,自动队伍大小为 C。没有有效逐波数组时,先判断 G 是否至少为 1.5 × C:满足就以 G − C 为倍率前的基数,否则仍用 G;随后才乘 M 并整数化。
例如 B = 4、M = 1.5,得到 G = 10、C = 5,结果是把 (10 − 5) × 1.5 转为 7 点。如果先把普通预算算成 6,再乘 2.5,会误得 15 点;如果先算 10 × 1.5 再扣 5,又会误得 10 点。两种算法都把运算放错了阶段。
有效逐波数组覆盖的是这个默认预算计算的基数,Flag 决定的则是是否插入自动队伍。假设大波的数组值为 6、M = 1.5,整数预算便是 9,不再从这个 6 中重复进行默认的大波预留;若 Flag: true,自动队伍依然可以另外存在。WaveManager.Flag 又是第三件事,它管理大波前的等待,不会往队伍里添加旗帜僵尸。
手动记录也可以占用预算
假设本波有 5 点预算,作者另外手动安排了一只路障。如果没有其他特殊处理,点数池仍然可以拿这 5 点继续抽。手动写了一只,并不自动意味着随机部分少来一只。
若希望手动安排也计入开支,可以开启默认关闭的 SubtractMode。例如这条路障记录的扣点价格为 3,那么扣除后便只剩 2 点供普通抽取。
扣多少点也有查找顺序:手动记录自己写了 Points,先用这个值;没有写,就找池中对应类型的价格;仍然找不到,才按 0 处理。只有本波满足难度条件的手动记录参与这项扣除。由此可见,打开扣点模式以后,随机追加的余量取决于手动记录的价格,并不是简单减去手动僵尸的只数。
末波可能补进从未抽到的候选
再考虑一种特殊情况:池中一直放着路障,权重也不是 0,但前面恰好一次都没抽到。到了最后一波,游戏会不会让它继续缺席?默认开启的 Protected 就是为这种情况准备的。
它会检查各个池条目的历史,将权重非零、此前池中生成次数为 0 的条目补入一次,并扣去价格。为便于说明,假设检查时还剩 2 点,唯一需要保底的是价格为 3 的路障,而且所有行都可用。虽然 2 点买不起它,保底仍会补入,预算也随之变为 2 - 3 = -1。

为什么买不起还能补进来?因为保底发生在普通抽取的解锁与预算检查之前,也在手动记录扣点之前。尚未解锁或预算不足,都不能用来排除一次保底。
还要特别留意“从池中生成过”这几个字。假如前面出现的路障来自手动列表,就不算池条目已经抽到;反过来,只要该条目此前已从池中生成过,末波便不会再为它保底一次。保底记的是池条目的历史,不是玩家在场上见过哪些僵尸。
把所有来源放在一起结算
做一个组合这些规则的假设例子。某个末波已算出 7 点整数预算;本波另有一只手动路障,开启 SubtractMode 且扣价为 3;池中另一个价格为 5、正权重的条目此前没有生成过,需要末波保底。假设各行可用,且没有其他保底条目。
- 手动路障已在本波记录中,稍后才结算它的 3 点扣点。
- 保底先补入价格 5 的条目,余额从 7 变成 2。
- 再扣手动路障的 3 点,余额变成 −1。
- 普通抽取要求余额大于 0,因此不再抽取;手动与保底记录都保留。
这个负数不是下一波需要偿还的点数,也不会反过来删掉已排入的僵尸。它只说明在这个顺序下,特殊安排已超过随机预算。预算限制的是普通抽取的开支,不能理解成整波所有僵尸的上限。
保底按池条目逐个检查,多个未生成的正权重条目可以各补一次,花光预算并不使这轮检查停止。把同一类型写成两个池条目时,各自的生成历史也要分别看;某一条目已经抽到,不代表另一条目的保底资格被消耗。自动队伍里的普僵与旗帜同样不能替代池条目的生成历史。
这里记录的是初始化过程中的生成,不是战斗中的击杀。早先从池中排入的僵尸即使很快被清除,也不会因此重新获得末波保底资格。复盘时应按来源核对手动记录、自动队伍、保底插入和普通抽取,再分别核对扣点。
从哪一行进入
固定行与随机行
前面一直在讨论来什么僵尸,现在再看它们从哪里进入。一条记录写着 Row: 2,会落在哪一行?答案是第 3 行,因为这里的行号从 0 开始。常见五行场地中,最上行编号为 0,最下行为 4。
如果没有填写记录的 Row,或者写成 Row: -1,游戏会等到这只僵尸出场时再选行。它先从当前可用、且不受行冷却限制的行中随机选一行;如果所有可用行都在冷却,则选择冷却值最低的一行。
固定行也不是无条件生效。指定行不合法、不可用或受冷却限制时,同样会回退到随机路径。因此,波表写了固定行,说明的是记录中的安排;最终出场仍然要通过运行时检查。
此外,关卡顶层还有一份 Row 数组,用来记录各行状态,其中状态值 2 表示不可用行。它和僵尸记录中的 Row: 2 含义不同:前者在说一行处于什么状态,后者在说一只僵尸指定去哪一行。
开局选行为什么比较均匀
如果连续观察点数池生成的僵尸,可能会觉得它们在各行分得比较开。这与初始化时使用的平滑选行器有关。点数池和自动旗帜队伍选行时,会记录每行距最近两次被选中已有多少次选择,再据此调整权重,让近期选得少的行更容易被选中。
可以把它理解为一本不断更新的选行记录。读到下一波时,这本记录不会清空;手动固定行也会更新它。因此,某一波的行分布还会受到前面记录的影响。
但平滑选行并不要求“每行轮到一次才能重来”。刚选过的行仍有可能再次被选中,五只僵尸也不一定恰好每行一只。另外,类型抽取和平滑选行共用同一条关卡局部随机数流,改动前面的候选或记录,也可能使后面的结果发生变化。
平滑权重可以怎样验算
把可用行的原始权重归一化,记某行占比为 p;五行都可用且等权时,p = 0.2。再用 a、b 分别表示距最近一次、倒数第二次选中该行的选择次数。暂不考虑单精度取整误差,程序计算的两个修正量可以写成:
- 最近一次的修正:
u = 2 × p × (a + 1) − 1。 - 倒数第二次的修正:
v = p × (b + 1) − 1。 - 合并后先限制范围:
k = clamp(0.75 × u + 0.25 × v, 0.01, 100)。 - 该行用于此次抽取的权重:
q = p × k。最后仍要用各行 q 的总和归一化,q 本身不是最终概率。
这里的 clamp 就是低于下限取下限、高于上限取上限。五行等权时,假设某行 a = 4、b = 9,可以算出 u = 1、v = 1,因而 q = 0.2。另一行若刚被选过,a = 0、b = 5,则 u = −0.6、v = 0.2,合并为 −0.4,按下限取 0.01,最终 q = 0.002。后一行的相对权重被大幅压低,但没有变成 0。
这就是“比较均匀却仍能连行”的原因:算法压低近期重复,保留小概率;距上次选择的间隔变长后,权重又会提高。它记录的是选择次数,不是时间,也不是这一行一共出现过多少只僵尸。一次选行后,参与历史更新的行增加计数,被选行把最近间隔移入第二段历史,并把最近间隔归零。波间保存的正是这些历史量。
类型抽取后通常还会消耗随机数来选行,下一只的类型抽取才继续。因此,同一种子下增加一只前置僵尸、改变价格或开启自动队伍,都可能改变后续随机数的用途,连带改变后面多波的结果。固定种子可以复现实验,但不能把“同种子、不同配置”理解为只改变一处、其余随机结果必定不动。
灵魂选行究竟看什么
Row: -2 是另一种情况。在 EASY、MEDIUM 下,它仍按普通随机规则选行;到了 HARD、IMPOSSIBLE、UNBALANCED,才会使用 Soul 评分。游戏在每波开始时计算一次植物布局快照,然后从可用行中选分数最低的一行;若最低分并列,就取最上面的一行。
我们用一个容易计算的阵容来理解它。假设五行都可用,第 1、3、5 行各有一棵满耐久坚果,第 2、4 行为空,场上没有其他植物。单棵坚果的贡献按 5 × 当前耐久 / 最大耐久 作整数计算,满耐久时就是 5 分。于是五行依次得到 5、0、5、0、5。
第 2、4 行同为最低分,该选哪一行呢?按照并列时取最上行的规则,会选择第 2 行。

现在稍微改变一下情形:第 2 行已经来了一只本波的僵尸,后面的灵魂记录会转而选择第 4 行吗?在这个例子中不会。评分用的是本波开始时的植物快照,新来的僵尸并没有改变这份评分。因此,多条灵魂记录可能连续选中同一行。
真正的评分还会考虑豌豆射手与豌豆坚果的邻行 Dd 基因支援,但并没有把所有植物的输出、控场、阳光与主动技能都算进去。所谓“灵魂弱行”,只是这份评分最低的一行,不一定就是你在实战中觉得最难防守的那一行。
灵魂行与普通随机行还有一个区别
HARD 及以上的灵魂分支检查行是否可用,却不采用普通固定行的“冷却必须不大于 0”条件;选出最低分可用行后直接进入生成。因此,不能把普通选行的冷却回退规则用于灵魂行。在低难度下,Row: -2 转为普通随机路径,才按对应的冷却规则处理。
普通随机路径在所有可用行都冷却时,最低冷却值若并列,也取最上面的一行;如果根本没有可用行,这次生成可能无法完成。初始化的平滑分布、出场时的冷却检查、灵魂分数比较是三种不同的选择依据,并非同一个“随机行算法”的三种说法。
坚果的整数评分也意味着耐久变化不总是平滑体现到分数上。例如耐久比例为 80% 时贡献 4 分,降到 79% 后是 3 分;19% 时贡献已为 0。分数相同的两行仍由上方行获选,所以“补了一点耐久”只有越过整数分界时才可能改变排序。其他植物即使在实战中很强,只要未进入这份评分规则,也不会按战斗价值自动加分。
清得越快,下一波就越早吗
知道了每波有哪些僵尸、从哪一行来,还剩最后一个问题:下一波什么时候来?这部分由 WaveManager 参与控制。InitTime 设置首次等待,Counter 设置波间计数器,Factor 则参与计算提前刷新的阈值;其中的 Flag 子配置控制进入大波前的节奏。
我们先看提前刷新是怎样发生的。每波生成后,游戏记下本波符合条件的僵尸的初始统计值,再乘 Factor 得到阈值。之后,计数器走过 200 以上、剩余值仍大于 100 时,如果本波剩余统计值降到了阈值或以下,就把剩余等待改为 100。
这里可以做一个只看数字的例子。假设初始统计值为 100,系数为 0.25,阈值便是 25;计数器从 2500 走过了 300,还剩 2200。此时若剩余统计值为 20,三项条件都满足,计数器便从 2200 改为 100。

如果只走过 150,即使统计值已经降到 20,也不能在这个时刻触发上述分支。这说明提前刷新要同时看清怪进度与计数器条件,不能只看场上还剩多少僵尸。
0.17q 的默认首次等待是 1800 个逻辑更新单位,普通波计数器为 2500~3100、系数为 0.25,大波计数器为 4000、系数为 0。关卡可以覆盖这些设置,例如第 48 关将普通波和大波的系数都设成了 0.5~0.65。这些单位不能直接当作毫秒或显示帧数。
还需要说明两点。其一,统计时会检查波次归属与实体状态,不能拿全场僵尸头数替代这里的统计值。其二,触发后只是把剩余等待缩短到 100,并不会当场生成下一波;大波提示与额外计数器也会影响实际出场时刻。本站波表目前没有模拟这段实时计时。
提前刷新的三个条件要同时成立
把本波初始统计值记为 H₀、当前值记为 H,抽定的系数记为 F,初始计数器记为 C₀、剩余计数器记为 C。程序先把 H₀ × F 转为整数阈值 T,之后在满足 C > 100、C₀ − C > 200、H ≤ T 时,把 C 改为 100。
这几个比较的边界会影响判断:已走过恰好 200 不够,剩余恰好 100 时也不会再缩短;但 H 恰好等于 T 就能通过。例如 H₀ = 101、F = 0.25,阈值是整数 25,而非 25.25。范围形式的 Counter 与 Factor 在设置这段波间等待时抽定,并不是每次检查都重新抽一个阈值。
F 越大,允许留下的统计值通常越多,因而越容易提前刷新;默认大波前 F = 0,则要求统计值降至 0 或以下才能走这个提前分支。F = 0 不等于停止计时,也不等于必须清场才能来下一波。 即使始终未达到提前刷新阈值,正常计数器仍会推进,计时及其他生成条件满足后照样可以出下一波。
H 的统计只遍历符合波次归属、实体标记与状态条件的对象,再加上各对象通过虚方法返回的数值。它既不是全场头数,也不能未经核实就写成所有僵尸血量的简单相加。因此,看到前面波次还有僵尸未被清除,不能认定当前波的提前刷新一定被阻止;反过来,击杀一只醒目的僵尸,也不能只看外观推断 H 降低了多少。
大波提示为什么会延长等待时间
波后选用普通还是 Flag 参数,看的是即将到来的下一波。例如第 9 波生成后,若第 10 波是大波,这段等待就采用 Flag 配置;不是等第 10 波已经生成,才用它计时。首次等待另由 InitTime 设置。
进入大波前,主计数器走到 5 时会启动提示,并把提示计数器设为 810。提示计数器大于 60 的阶段会挡住后续主计数处理;降到 60 时又会把主计数器设为 1,随后才继续检查生成条件。波生成后还会设置一个值为 100 的额外生成门槛计数器,实际生成下一波时也要检查它。
因此,“余量改成 100”只能解释主计数器的一次变化,不能直接翻译为“恰好 100 单位后出怪”。尤其进入大波时,还要经历提示流程。这些数值表示逻辑更新过程,不应换算成未经验证的墙钟秒数。
在实战中,集中输出会让当前波更早达到阈值,也可能更早带来下一波压力;留下一只僵尸也未必拖得住时间,因为它可能属于前面的波次、统计贡献已不足,或者普通倒计时即将结束。想解释多波同时在场,应同时核对波次归属、阈值、主计数器和大波提示,而不只看场上还站着几只。
回到第 1 关,自己算一遍
前面的规则已经足够用来读一份简单关卡了。我们回到第 1 关:它共有 10 波,没有顶层手动列表,开启了自动旗帜队伍。普僵、路障的价格分别为 1、3,权重同为 4000,路障从第 2 波解锁。
如果选择 MEDIUM,倍率为 1.5,那么路障最早能在哪一波参与普通抽取?先把前四波的预算依次算出来:
- 第 1 波:
1 × 1.5转成整数,得到 1 点。 - 第 2 波:
(1 / 3 + 1) × 1.5,得到 2 点。 - 第 3 波:算出 2.5,截去小数后仍是 2 点。
- 第 4 波:
(3 / 3 + 1) × 1.5 = 3,终于有 3 点可用。

答案是第 4 波。第 2 波虽然已经解锁,但 2 点还买不起价格为 3 的路障;第 3 波同样不够。解锁只解决“允许抽”的问题,预算还要解决“买得起”的问题。 两项都满足以后,是否真的抽中,仍然取决于抽取结果。
再来看第 10 波。此时 i = 9,基础预算是 9 / 3 + 1 = 4,经过大波放大后成为 10。附加普僵数为 9 × 1.5 / 3 = 4.5 截成 4,连同旗帜共 5 只。
由于 10 ≥ 1.5 × 5,先从基础预算中扣掉 5,再乘 1.5,转成整数得到 7 点。这 7 点供后续点数池处理,自动队伍则另有 1 只旗帜和 4 只普僵。计算到这里,记得把这两部分分开,才不会漏掉自动队伍,也不会把预留点数重复扣除。
现在可以打开第 1 关的 MEDIUM 种子示例,展开完整波表来核对。本站当前模型中,种子 123456 的第 4 波是一只位于第 3 行的路障;第 10 波除自动队伍外,池中还抽到 4 只普僵与 1 只路障,正好花掉 7 点。
最后留一个小练习:改到 UNBALANCED,第 2 波能否抽到路障?这一关的倍率数组是 1、1.5、2、2.5、4,最高档覆盖了默认的 3。可以先代入计算,再与下面的结果比较。
用 4 倍计算,第 2 波有 5 点。此时路障既已解锁,预算也足够,因此能够参与抽取。它从第 4 波提前到第 2 波获得资格,并不是解锁波次变了,而是难度倍率让预算提前达到了价格要求。
用波表验证时,还要知道什么
阅读本站出怪解析时,可以先选择难度,再分清确定数量、候选类型和出场时选行。问号表示点数池数量还没有经过初始化模拟;“可选 1/2/3/4/5 行”列出的是同一条记录的可能落点,不能读成五行各来一只。
开启“模拟波表”后,输入的无符号 32 位整数会初始化本站重建的 MT19937。同一实现下,关卡、难度和种子相同,便能得到相同的模拟结果。这使我们可以反复检查一次抽取究竟花了多少点、后面又剩下哪些候选。不过,它与我是僵尸无尽的 12 位分享代码用途不同,本站也没有还原游戏上游怎样产生这份种子。因此,输入一个数得到的波表,不能直接认作某次实机对局的出怪。
重建中还有两处差异。原程序在首次平滑抽取前没有初始化行权重,本站将其规范为 0;原程序先更新手动固定行的历史,再作难度过滤,当前模拟则只记录所选难度的有效固定行。如果关卡混用了这些记录,逐行结果就可能不同。出场时的行冷却、随机选行、Soul 快照与波间计时,同样不在这份初始化模拟的范围内。
官方页面只分析基础 level{id}.json,类似 level34_1.json 的文件属于相关子关卡或变体,不能混进基础波表;创意工坊则解析当前选中的子关卡。若页面提示没有标准出怪表,意思是没有常规波表可以展开。控制器、Boss 或预置实体仍然可能产生僵尸,需要继续看相应关卡的其他安排。