为什么没生效 · Debug
这一页专门回答一件事:CD2 改了却看不到效果,先查什么。内容是我们自己踩过的坑与给新作者的建议,不属于官方文档的翻译内容。
按症状排查:先定位,再深入
「没生效」不是一个问题,是一类问题。先在下面找到最像你的一条,它会告诉你该看哪一节。
| 症状 | 最可能的原因 | 先看 |
|---|---|---|
| 完全没有变化 | 文件没加载 / 字段名或位置不对 / Vars 里同名变量重复定义 | 完全没有变化 |
| 数值变了,但效果和想象不一样 | 被别处覆盖 / 池子里没有它 / 变量取不到值 / 被当前官难的值补上 | 效果和想象不一样 |
| 该出现的没出现(刷怪 / 波次) | 条件写错了场景 / 整数字段写成了小数 / 被上限或冷却拦住 | 该出现的没出现 |
| Mutator 报错,或一直是初始值 | 输入输出类型不匹配(数值直接喂了布尔字段最常见) | Mutator 不工作或报错 |
| 本地能玩,朋友进不来 | 用的是 Enemies 而不是 EnemiesNoSync | 联机时出问题 |
九步顺序排查
不确定自己属于哪一类,就按这个顺序排除。顺序是从便宜到贵——先排除那些不用进游戏就能看出来的错误。
不需要排查「版本不支持」:字段在当前版本不存在时,CD2 会直接报错并说明原因,不会安静地失效。
- JSON / 文件格式:粘进 CD2 面板时如果报 JSON 错,先修语法。见基础部分 · 难度文件要求。
- 字段名与结构:CD2 对字段名大小写不敏感(同一个字段写成
MUtate或mutate都能跑),所以大小写本身不是错误;要查的是名字写错、层级放错。见第七节。 - 字段放在正确位置:同名字段在不同模块里含义不同,要放进它该在的那一层。
- 字段的类型对不对:整数字段别写小数——虫量上限写成小数会直接刷不出敌人;见模块 · 虫量上限。
- Mutator 的输入输出类型:数值型输出不能直接喂给要布尔信号的字段——这是本站出现最多的坑。
- 条件是否写对了场景:条件依赖游戏状态(人数、时间、阶段、补给次数)。如果你写的是「按人数」「按多人」的条件,单人测试当然不会触发——那是条件本身用错了场景,不是没生效。
- Pool / WaveSpawner 是否还有别的限制:上限、池子内容、生成位置、冷却都可能挡住你。
- 是否涉及客户端同步:这条只看一件事——你用
Enemies还是EnemiesNoSync。见本地能玩,联机出问题。 - 用 Vars / Watch 看实际值:到这一步还找不到,就把值显示在界面上,直接看它到底等于几。
完全没有变化
- 文件到底有没有被用上:先 Save,再进游戏;见 FAQ · 改完难度文件,为什么游戏里没变化?
- 字段名或位置写错了:见七、常见错误写法。
Vars里定义了两个同名的变量:会出问题。具体表现与原因本站待核实,不做推测——但变量名保持唯一总是对的。- 仍然没头绪:把值接到
Vars+Watch上直接看,见一、先用 Vars + watch 字段把逻辑调通。
数值变了,但效果和想象不一样
- 引用了
Vars里不存在的变量,或变量之间循环依赖:CD2 不会报错,但依赖它的那个单位 / 效果会直接消失。这类问题只能靠Watch看变量是不是取不到值——见一、先用 Vars + watch 字段把逻辑调通。 - 是不是被别处覆盖了:同一个值可能被多处写入,用
Vars收敛成一个来源。 - 被「当前选择的官难」补了值:部分内容 CD2 会去读玩家当前选择的官方难度来补默认值——所以你的改动可能被补值盖住。见新手入门 · 一个难度文件里有什么与模块 · 难度设置。
- 改了敌人,但它不在任何池子里:先看模块 · 怪池。
- 生成数量类字段容易多出一只:见六、生成数量与点数。
该出现的没出现:刷怪与波次
- 整数字段写成了小数:虫量上限之类的整数字段填小数会导致敌人根本刷不出来——见模块 · 虫量上限。
- 条件根本没有成立:Trigger 要的是布尔信号,数值输出要先经
TriggerOnChange——见Mutator · 触发类。 - 只触发了一次,之后再也不触发:多半是
Or里混进了「一直为 true」的长信号,把输出钉死了——见七 ②。 - 一次性的击杀事件是瞬时的,要先用
TriggerFixedDuration拉成持续窗口:见五、「一瞬间」的布尔信号。 - Spawner 与难度点数的关系:见敌人生成器 Spawner 与六、生成数量与点数。
- 被上限或冷却挡住:见Mutator · 敌人生成冷却。
不工作或报错 · Mutator
- 最常见的一条:把数值型 Mutator 直接喂给需要布尔信号的字段——见七 ① 与 FAQ · 我写的 Mutator 没反应,怎么查。
- 类型要对齐:Float / Int / Boolean / String / 数组,输出必须和下游字段的要求一致。
- 下标从 0 开始:见二、CD2 的下标从 0 开始。
- 它不是瞬时响应的,别用一帧的结果判断对错:见三、Mutator 不是瞬时响应的。
本地能玩,联机出问题
客机能不能进,只看一件事:难度里写的是 Enemies 还是 EnemiesNoSync(见模块 · 敌人配置)。
Enemies:客户端必须安装 CD2,否则任务一开始就会被断开。EnemiesNoSync:这是让没装 CD2 的客机也能进来的唯一要求。代价是客机体验不到完整功能,会出现异常(包括但不限于视觉表现)。- 要不要为了野房改成
EnemiesNoSync,是作者自己的取舍:功能丢失可能让玩家误解你设计这个难度的本意。通常做法是另存一份「野房版」,而不是把原版直接换掉。 - 已知现象(原因未知):在固定单位(
ShootingPlant这类)的resistance里用EnemiesRecentlySpawned动态调整抗性,会导致有无 CD2 的客机都在开局掉出,但可以连回来(即使用了EnemiesNoSync也一样)。本站没有查明原因,只记录现象;遇到类似情况建议先移除这段逻辑二分定位。 - 与 CD1 共存、公开匹配的规则:见 FAQ · 能同时使用 CD1 和 CD2 吗 与 FAQ · 我的难度能给别人玩吗。
深挖:七节常见坑
下面七节是更细的原因与写法对照。上面的分诊表把你送到这里之后,再从对应一节读起。
一、先用 Vars + watch 字段把逻辑调通
Vars 是 CD2 里最强大、也最容易被新作者忽略的模块。它远不只是个调试工具,至少能同时帮你做三件事:
- 判断自己的代码有没有问题:把想验证的表达式接进一个变量,打开
Watch,它的值就会直接显示在游戏 UI 上。关键在于:一旦代码写出问题,这个变量就会什么都不显示——所以「屏幕上找不到它」本身就是一个明确的报错信号。对刚上手写难度的作者来说,这一点的价值怎么强调都不过分。 - 储存信息,让代码更简洁:变量定义一次之后,就能在任何接受同类型值的地方用
{"Var": "变量名"}引用。把重复出现的表达式抽成一个变量,代码会短很多,日后要改也只需要改一处。 - 让模块之间能够通信:所有变量在同一时刻统一更新,因此它在各个模块里读到的值始终一致。
一个必须知道的限制:变量之间不能形成循环依赖。
- 允许 —— 链式引用:
A → B → C。C 的Value里用{"Var": "B"},B 里再用{"Var": "A"},一路单向往下走多少层都行。 - 报错 —— 循环引用:
A → B → A。B 引用了 A,而 A 又回头引用 B,绕成一个圈。
即使你用手动制造延迟的办法把两者在时间上错开,循环仍然会直接报错——变量是用来「汇总」的,不是用来互相串的。
写 Mutator 最难受的是「不知道到底有没有生效」。推荐做法:在 Vars 里定义一个变量,把想验证的表达式接进去,然后在游戏里用 watch 实时看它。
"Vars": {
"调试-击杀超过10": {
"Type": "Boolean",
"Watch": true,
"Value": {
"Mutate": "IfFloat",
"Value": {"Mutate": "EnemiesKilled"},
">": 10,
"Then": true,
"Else": false
}
}
}布尔变量最适合当 watch:你一眼就能看出条件在哪个时刻变成 true。比起反复改难度、进游戏试,这条路快得多。
二、CD2 的下标从 0 开始
这是很容易踩的一个坑:CD2 里凡是「按序号取第几个」的字段,序号都从 0 开始数。
SelectFromArray:Select返回0→ 取数组第 1 个;返回1→ 取第 2 个,以此类推。Sequence:同理,0对应序列的第一项。
写映射表时不要按「第几个」去数,要按「从 0 开始的序号」去数——差一位是最常见的调试来源。
三、Mutator 不是瞬时响应的
Mutator 采用轮询机制,信号变化到被识别之间约有 0.5 秒延迟。用击杀数、血量这类高频变化的量做精确计时时,要把这段延迟算进去。
四、随机类 Mutator 的返回值是持续变化的
Random、RandomChoice 等随机类 Mutator,只要被求值就会返回一个结果——在设定范围内每一次取值都可能不同。也就是说,它们不是「摇一次就固定下来」,而是随着求值不断变化。
如果希望一个随机结果固定下来,有两种做法:
- 使用
RandomChoicePerMission:它只在任务开始时抽取一次,之后保持不变。 - 用
LockFloat/LockBoolean/LockString等锁定类 Mutator 把随机结果锁住。如果还想把这个值存下来反复引用,可以把它放进Vars,这样也能和其它 Vars 配合,做出更复杂的效果。
五、「一瞬间」的布尔信号能干什么
很多触发器(例如 TriggerOnChange)产生的 True 只持续一瞬间,但它并不等于没用:
- 大部分布尔字段会被一瞬间的信号正常激活一次;
- 波次生成器这类带
Interval的字段要留意:Interval比信号持续时间长时,它可能一直在等下一次,结果根本不生成;Interval设得短,一瞬间的信号也能触发一次生成。
结论:能不能用取决于目标字段,建议实际跑一次再下结论。确实需要一段稳定的持续窗口时,用 TriggerFixedDuration 把信号拉长。
六、生成数量与点数:两个容易踩的坑
实际会多刷一只 · MaxSpawnCount
波次的实际点数充足时,MaxSpawnCount 限制的数量会多生成 1 只——写成 3 时可能看到 4 只。做数量控制时要按这个来预留余量。
用「母体 + 自定义变种」控制生成,而不是直接调敌人本身
如果想控制某种敌人刷出多少、多稀有,直接在该敌人身上设置数量、稀有度或点数并不可靠——它仍然可能被波次刷出来。
更稳的做法是:放一个母体单位,用它的自定义变种(CustomVeterans)字段去改成你想要的目标敌人。这样数量与稀有度的控制就落在母体上,目标敌人本身不再受波次直刷的影响。
另外,WaveSpawner 的波次实际点数,推测约为 Difficulty × EnemyCountModifier(此式未在官方文档中确认,仅供参考)。
七、常见错误写法
❌ 是错误写法,✅ 是修正写法。
① 把数值型 Mutator 直接喂给需要布尔信号的字段
In、If 的 Condition 等字段要求布尔信号,而 DwarvesDown、EnemiesKilled 这类 Mutator 返回的是数值。数值输出必须先经 TriggerOnChange 转换。
❌ "In": {"Mutate": "DwarvesDown"}✅ 用 TriggerOnChange 读取;只想在数值上升时触发就加 RiseOnly:
"In": {
"Mutate": "TriggerOnChange",
"RiseOnly": true,
"In": {"Mutate": "DwarvesDowns"}
}② 把「一旦成立就一直为 true」的长信号直接 Or 进去
你想让 A 达成时触发一次、B 达成时再触发一次,于是把两者写进 Or——
结果只触发了第一次,之后再也没动静。
为什么:Or 输出的是状态,不是事件。像 SecondaryFinished
这类信号一旦成立就一直为 true(直到任务结束);它一进 Or,Or 的输出就被
永久钉在 true——下游那种「只在 false→true 那一刻动作」的逻辑再也等不到第二次跳变,
于是卡在「只能激活一次,不能再次进位」。
❌ "Or": {
"Mutate": "Or",
"First": {
"Mutate": "TriggerOnChange",
"RiseOnly": true,
"In": {"Mutate": "EnemiesKilled"}
},
"Second": {"Mutate": "SecondaryFinished"}
}✅ 先把长信号裁剪成一段窗口再合并。TriggerFixedDuration 的官方示例讲的就是这件事:
「SecondaryFinished 在次要目标完成后会一直保持 true 直到任务结束,而这个触发器把这段长信号
裁剪成只有 20 秒的窗口」:
✅ "Second": {
"Mutate": "TriggerFixedDuration",
"N": 20,
"In": {"Mutate": "SecondaryFinished"}
}✅ 如果你要的是「每轮只触发一次」或「最多 N 次」,不要用 Or 去计数:用
TriggerOnce(再加 Reset 就能再次触发)或 TriggerNTimes(N 次)——
它们本来就是处理 False → True 跳变的。
类型别弄反(这里最容易错):TriggerOnChange 的 In 只收数值,
它负责把「击杀数变化」这类数值变化转成布尔脉冲,不检测布尔信号的变化;其余触发器的
In 收布尔信号,数值必须先经 TriggerOnChange 转换。完整字段见
Mutator · 触发类。