【开发者日志】第二篇:涌现到底发生了没有
#聚光灯gamejam开发者日志
我们是新人团队 EmbersStudio,这里是第二篇开发者日志
![[表情_可爱]](https://img-tc.tapimg.com/market/images/86e3f28f5dddd78c532e8c470622c845.png)
本期进展
Day 3 到 Day 6,规则系统的内核算是跑完了。规则引擎会去扫 data/rules/,把规则牌 JSON 加载进来;它订阅事件总线上的所有事件,按条件筛,再执行动作。动作外面有四道闸门卡着:单牌冷却、单牌每秒上限、每帧全局预算、最大执行深度。默认牌池目前三张:
- 暴击赏金:暴击时掉 3 枚金币
- 金币弹幕:拾取金币时发 1 颗子弹
- 子弹回响:子弹命中时暴击率 +5%,持续 1 秒
外围也做了不少:实体对象池(子弹和金币预生成,命中/拾取后回收复用)、日志系统的文件分片和手动导出、HUD 的连锁表现(数字跳动、边缘发光、边框脉动,还有一段代码合成的占位音,音调会跟着连锁升高),以及当前暴击率的显示
![[表情_吃瓜]](https://img-tc.tapimg.com/market/images/d07b262774c8a022a7dddbc39683da6b.png)
不过这一期真正想聊的,不是我们做了多少东西,而是另一个问题:涌现到底发生了没有
“感觉挺好玩”不能当答案。为了不靠感觉,我们做了两件事找证据
第一件,自举闭环测试。只装“暴击赏金 + 金币弹幕”两张牌,把玩家自动攻击屏蔽掉,然后手工种一次暴击。这样之后场上唯一还能产生 ON_CRIT 的东西,就是金币弹幕自己发射的规则弹。如果链还能继续转,就说明这个闭环是自己给自己上电,而不是靠设计者把每一环都补齐。结果链没断:暴击赏金触发 3 次以上,金币弹幕 2 次以上
第一版其实是失败的,原因值得记一笔。暴击赏金的冷却设了 0.2 秒,比一个连锁周期还短,第二次暴击会被“冷却中”挡掉,链转一轮就死。后来把玩家和敌人拉开 100 像素,让金币和子弹各飞一段,单轮周期长于冷却,链才稳定自持。这里面的耦合是真的:闭环能不能自持,取决于“一轮周期”和“单牌冷却”的比值
第二件,批量仿真。写了个脚本枚举构筑 × 顺序 × 规则开关 × 随机种子,每局落一行 JSONL,跑了 48 局。crit 指“规则弹能否暴击”这个开关:
| 构筑 | crit | 真实深度 | 执行次数 | 击杀 | 拾取 |
|---|
| 暴击赏金 + 金币弹幕 | 开 | 12.0 | 24.0 | 9.5 | 21.0 |
| 暴击赏金 + 金币弹幕 | 关 | 12.0 | 16.0 | 7.5 | 15.5 |
| 暴击赏金 + 子弹回响 | 开 / 关 | 4.0 | 15.0 | 3.5 | 11.0 |
| 金币弹幕 + 子弹回响 | 开 / 关 | 12.0 | 18.5 | 6.5 | 6.5 |
| 三张牌 | 开 | 12.0 | 34.0 | 8.5 | 17.5 |
| 三张牌 | 关 | 12.0 | 24.0 | 6.5 | 12.5 |
三个能直接读出来的结论:
一,顺序敏感性是 0。同一副牌换任何顺序,六项数字完全相同,连小数都一样。原因在牌池本身:三张牌分别监听 ON_CRIT、ON_PICKUP_GOLD、ON_BULLET_HIT 三个不同事件,而执行顺序只在“同一个事件下挂了多张牌”时才有意义。我们规划里写的“调序影响结果”,在当前牌池下根本体现不出来,得等出现监听同一事件的牌,才会变成真机制
二,决策影响力是显著的。换牌的差异远大于随机噪声:执行次数在 15 到 34 之间,差 2.3 倍;击杀 3.5 到 9.5,差 2.7 倍。至少在这一版牌池下,选牌不是装饰
三,我们差点拿错证据。HUD 上那个“连锁”数字在仿真里能冲到 17 到 19,而真实执行深度只有 12,绝大多数构筑恒等于 12。差出来的那部分,就是被闸门拦下的触发。那个数字可以用来看热闹,不能用来证明涌现
顺带一条前提:同一个参数连跑两次,48 行产物逐行完全一致。如果连确定性都没有,“换牌有差异”这句话就没法成立
问题与风险
最大的风险来自上面第三条。48 局里被拦下的原因只有“深度满”一种,说明当前真正卡住我们的是执行深度 12,实体上限和每帧预算都没碰到。这有两种解释:往好里说,系统健康,闸门在保护性能;往坏里说,我们正在用闸门裁剪涌现,把本来能连下去的东西提前掐掉。现在还没有证据能区分这两者,这是下一阶段必须回答的问题
![[表情_叹气]](https://img-tc.tapimg.com/market/images/9598a6cd4e4eba8470eb1520ed6a885d.png)
第二个风险是牌池太小。三张牌能证明机制成立,证明不了玩法成立。“顺序敏感性为 0”这个结论,一部分也是牌池小带来的直接后果
![[表情_豹哭]](https://img-tc.tapimg.com/market/images/7a440cbe4c6a7f5ef638d4cd933e4c15.png)
接下来是这一阶段最贵的一课:
Day 6 试玩时我们发现,金币一直在掉,但子弹到后面就不发射了。查下来是一个死锁:连锁的空闲计时清零被写在了执行动作之前,于是被深度拦下的触发也算作“有活动”,1 秒的超时永远等不到,连锁深度永久卡在 12,整套规则再也不执行任何动作
真正让人后背发凉的是它怎么溜过验收的:
当时的验收状态是:六套自测退出码全 0,静态检查全绿,真机和 headless 各跑 30 秒游戏时间,0 条 ERROR、0 条 WARNING。看起来无懈可击。但那个死锁在开局约 2.5 秒就已经触发,之后 27.5 秒规则系统一次动作都没执行。不报错,不超时,退出码照样是 0
继续往下查,原因实际上是 test_rules.gd 里限制层的用例没有 await。四组断言(冷却、每秒上限、深度上限、帧预算)全部在第一个 await 处挂起,而紧接着的 _report() 直接调用了 quit()。这些断言是在节点已经被释放之后才跑完的,退出码反映不到任何失败。所谓“六套自测全绿”,其中的 TestRules 实际只验证了三牌闭环,限制层从来没有被真正验证过
修复分两步:把空闲计时清零移到执行成功之后,被拦下的触发不再续期,卡满 1 秒后自动归零恢复;再把四个用例的 await 逐个补上。按真实语义模拟 30 秒做对比,修复前总共执行 12 次动作(正好等于最大执行深度)就彻底停摆,修复后 162 次且持续运转
这件事改了我们看待“测试通过”的方式。现在每加一条断言,先确认它真的会失败一次。一条从没红过的测试,跟没有测试是同一件事
还有两个小一点的局限,也得老实记上。仿真本身是简化的:玩家每帧回满血、敌人死了立刻补一个、暴击率拉满到 100%,它比的是连锁吞吐,不是真实生存压力下的表现。以及,我们还没有跑过一局完整的游戏
下期计划
- 牌池从 3 张扩到 10 张,然后重跑仿真。顺序敏感性和未预设组合,只有牌池够大才测得出来
- 重新评估最大执行深度 12 与单牌冷却,判断它们是在裁剪涌现还是在保护性能
- 进入内容阶段:房间与层数系统接管刷怪,新增规则牌改成只要加一个 JSON
- 把随机种子写进整局日志,做到按 seed 复现任意一局
- 等待美术素材准备完善后替换掉占位用内容
开发过程截图
连锁 12 顶到执行上限的那一刻。屏幕边缘的发光和边框脉动是这一期新加的连锁表现,测试时我们就盯着这圈光判断连锁还在不在跑
连锁 10。注意第四行“暴击率: 20%”,基础暴击率是 10%,多出来的 10% 是子弹回响连续触发叠上去的,说明规则确实在执行,不只是计数在动
连锁 11。画面上方红圈里的 0.7 是出生预警倒计时,敌人还没出现
杂谈
涌现 这个主题没有实物,很抽象,老实说我对这个选材没有十足把握,但考虑到时间与水平,也没有什么更好的主意,打算就这样做下去了,你们有什么相关的见解吗?或者有什么改进的建议?
![[表情_可爱]](https://img-tc.tapimg.com/market/images/86e3f28f5dddd78c532e8c470622c845.png)
至于上期本来说是要替换的
美术素材,截止至这篇日志发布,已经准备了许多,但还不够,所以第二篇发布时仍然没有替换美术素材
![[表情_豹哭]](https://img-tc.tapimg.com/market/images/7a440cbe4c6a7f5ef638d4cd933e4c15.png)
关于
游戏名称 嘛,我们还没有确定下来,取名字还是有些太困难了
![[表情_死机]](https://img-tc.tapimg.com/market/images/ffe3a048ef7da7deefaa56b91a1be2a8.png)
![[表情_死机]](https://img-tc.tapimg.com/market/images/ffe3a048ef7da7deefaa56b91a1be2a8.png)
![[表情_死机]](https://img-tc.tapimg.com/market/images/ffe3a048ef7da7deefaa56b91a1be2a8.png)
,有什么大佬能给点建议吗?
![[表情_星星眼]](https://img-tc.tapimg.com/market/images/49174c5480690cc48faea772549a81e0.png)