为什么没生效 · Debug

这一页专门回答一件事:CD2 改了却看不到效果,先查什么。内容是我们自己踩过的坑与给新作者的建议,不属于官方文档的翻译内容。

按症状排查:先定位,再深入

「没生效」不是一个问题,是一类问题。先在下面找到最像你的一条,它会告诉你该看哪一节。

症状最可能的原因先看
完全没有变化文件没加载 / 字段名或位置不对 / Vars 里同名变量重复定义完全没有变化
数值变了,但效果和想象不一样被别处覆盖 / 池子里没有它 / 变量取不到值 / 被当前官难的值补上效果和想象不一样
该出现的没出现(刷怪 / 波次)条件写错了场景 / 整数字段写成了小数 / 被上限或冷却拦住该出现的没出现
Mutator
报错,或一直是初始值
输入输出类型不匹配(数值直接喂了布尔字段最常见)Mutator 不工作或报错
本地能玩,朋友进不来用的是 Enemies 而不是 EnemiesNoSync联机时出问题

九步顺序排查

不确定自己属于哪一类,就按这个顺序排除。顺序是从便宜到贵——先排除那些不用进游戏就能看出来的错误。

不需要排查「版本不支持」:字段在当前版本不存在时,CD2 会直接报错并说明原因,不会安静地失效。

  1. JSON / 文件格式:粘进 CD2 面板时如果报 JSON 错,先修语法。见基础部分 · 难度文件要求。
  2. 字段名与结构:CD2 对字段名大小写不敏感(同一个字段写成 MUtate 或 mutate 都能跑),所以大小写本身不是错误;要查的是名字写错、层级放错。见第七节。
  3. 字段放在正确位置:同名字段在不同模块里含义不同,要放进它该在的那一层。
  4. 字段的类型对不对:整数字段别写小数——虫量上限写成小数会直接刷不出敌人;见模块 · 虫量上限。
  5. Mutator 的输入输出类型:数值型输出不能直接喂给要布尔信号的字段——这是本站出现最多的坑。
  6. 条件是否写对了场景:条件依赖游戏状态(人数、时间、阶段、补给次数)。如果你写的是「按人数」「按多人」的条件,单人测试当然不会触发——那是条件本身用错了场景,不是没生效。
  7. Pool / WaveSpawner 是否还有别的限制:上限、池子内容、生成位置、冷却都可能挡住你。
  8. 是否涉及客户端同步:这条只看一件事——你用 Enemies 还是 EnemiesNoSync。见本地能玩,联机出问题。
  9. 用 Vars / Watch 看实际值:到这一步还找不到,就把值显示在界面上,直接看它到底等于几。

完全没有变化

数值变了,但效果和想象不一样

该出现的没出现:刷怪与波次

不工作或报错 · Mutator

本地能玩,联机出问题

客机能不能进,只看一件事:难度里写的是 Enemies 还是 EnemiesNoSync(见模块 · 敌人配置)。

深挖:七节常见坑

下面七节是更细的原因与写法对照。上面的分诊表把你送到这里之后,再从对应一节读起。

一、先用 Vars + watch 字段把逻辑调通

Vars 是 CD2 里最强大、也最容易被新作者忽略的模块。它远不只是个调试工具,至少能同时帮你做三件事:

一个必须知道的限制:变量之间不能形成循环依赖。

即使你用手动制造延迟的办法把两者在时间上错开,循环仍然会直接报错——变量是用来「汇总」的,不是用来互相串的。

写 Mutator 最难受的是「不知道到底有没有生效」。推荐做法:在 Vars 里定义一个变量,把想验证的表达式接进去,然后在游戏里用 watch 实时看它。

json
"Vars": {
    "调试-击杀超过10": {
        "Type": "Boolean",
        "Watch": true,
        "Value": {
            "Mutate": "IfFloat",
            "Value": {"Mutate": "EnemiesKilled"},
            ">": 10,
            "Then": true,
            "Else": false
        }
    }
}

布尔变量最适合当 watch:你一眼就能看出条件在哪个时刻变成 true。比起反复改难度、进游戏试,这条路快得多。

二、CD2 的下标从 0 开始

这是很容易踩的一个坑:CD2 里凡是「按序号取第几个」的字段,序号都从 0 开始数。

写映射表时不要按「第几个」去数,要按「从 0 开始的序号」去数——差一位是最常见的调试来源。

三、Mutator 不是瞬时响应的

Mutator 采用轮询机制,信号变化到被识别之间约有 0.5 秒延迟。用击杀数、血量这类高频变化的量做精确计时时,要把这段延迟算进去。

四、随机类 Mutator 的返回值是持续变化的

Random、RandomChoice 等随机类 Mutator,只要被求值就会返回一个结果——在设定范围内每一次取值都可能不同。也就是说,它们不是「摇一次就固定下来」,而是随着求值不断变化。

如果希望一个随机结果固定下来,有两种做法:

五、「一瞬间」的布尔信号能干什么

很多触发器(例如 TriggerOnChange)产生的 True 只持续一瞬间,但它并不等于没用:

结论:能不能用取决于目标字段,建议实际跑一次再下结论。确实需要一段稳定的持续窗口时,用 TriggerFixedDuration 把信号拉长。

六、生成数量与点数:两个容易踩的坑

实际会多刷一只 · MaxSpawnCount

波次的实际点数充足时,MaxSpawnCount 限制的数量会多生成 1 只——写成 3 时可能看到 4 只。做数量控制时要按这个来预留余量。

用「母体 + 自定义变种」控制生成,而不是直接调敌人本身

如果想控制某种敌人刷出多少、多稀有,直接在该敌人身上设置数量、稀有度或点数并不可靠——它仍然可能被波次刷出来。

更稳的做法是:放一个母体单位,用它的自定义变种(CustomVeterans)字段去改成你想要的目标敌人。这样数量与稀有度的控制就落在母体上,目标敌人本身不再受波次直刷的影响。

另外,WaveSpawner 的波次实际点数,推测约为 Difficulty × EnemyCountModifier(此式未在官方文档中确认,仅供参考)。

七、常见错误写法

❌ 是错误写法,✅ 是修正写法。

① 把数值型 Mutator 直接喂给需要布尔信号的字段

In、If 的 Condition 等字段要求布尔信号,而 DwarvesDown、EnemiesKilled 这类 Mutator 返回的是数值。数值输出必须先经 TriggerOnChange 转换。

json
❌ "In": {"Mutate": "DwarvesDown"}

✅ 用 TriggerOnChange 读取;只想在数值上升时触发就加 RiseOnly:

json
"In": {
    "Mutate": "TriggerOnChange",
    "RiseOnly": true,
    "In": {"Mutate": "DwarvesDowns"}
}

② 把「一旦成立就一直为 true」的长信号直接 Or 进去

你想让 A 达成时触发一次、B 达成时再触发一次,于是把两者写进 Or—— 结果只触发了第一次,之后再也没动静。

为什么:Or 输出的是状态,不是事件。像 SecondaryFinished 这类信号一旦成立就一直为 true(直到任务结束);它一进 Or,Or 的输出就被 永久钉在 true——下游那种「只在 false→true 那一刻动作」的逻辑再也等不到第二次跳变, 于是卡在「只能激活一次,不能再次进位」。

json
❌ "Or": {
    "Mutate": "Or",
    "First": {
        "Mutate": "TriggerOnChange",
        "RiseOnly": true,
        "In": {"Mutate": "EnemiesKilled"}
    },
    "Second": {"Mutate": "SecondaryFinished"}
}

✅ 先把长信号裁剪成一段窗口再合并。TriggerFixedDuration 的官方示例讲的就是这件事: 「SecondaryFinished 在次要目标完成后会一直保持 true 直到任务结束,而这个触发器把这段长信号 裁剪成只有 20 秒的窗口」:

json
✅ "Second": {
    "Mutate": "TriggerFixedDuration",
    "N": 20,
    "In": {"Mutate": "SecondaryFinished"}
}

✅ 如果你要的是「每轮只触发一次」或「最多 N 次」,不要用 Or 去计数:用 TriggerOnce(再加 Reset 就能再次触发)或 TriggerNTimes(N 次)—— 它们本来就是处理 False → True 跳变的。

类型别弄反(这里最容易错):TriggerOnChange 的 In 只收数值, 它负责把「击杀数变化」这类数值变化转成布尔脉冲,不检测布尔信号的变化;其余触发器的 In 收布尔信号,数值必须先经 TriggerOnChange 转换。完整字段见 Mutator · 触发类。