方法论与洞察

LastStand v0.7.1 纯修复版:开发闭环复盘

入档:2026-07-29 项目:LastStand · 死守(Godot 4 波次生存 FPS) 项目目录(库外裸路径):E:\last-stand 发布地址:GitHub Release v0.7.1 itch.io:https://mr-salticidae.itch.io/last-stand 验证状态:⚠️ 全部 headless 自动化校验;没有真机试玩,手感变化未经验证;后期敌人分布调整(第 5 条)作者尚未打到第 25 波之后验收

事实记录(不可修改区)

以上先按 复盘事实先行原则 冻结。下文所有「有效」只指本次修复逻辑成立,不外推成「游戏平衡已调好」或「手感已改善」。

一句话结论

这一版的核心教训是:没有测试套件的项目,修复的置信度靠工具补;五个 bug 全藏在「两个看起来一样的数其实不是一回事」里——剩余数 vs 已落地数、整枪 vs 单弹丸、当前换弹 vs 被取消的那次换弹。

一、实际走通的链路

  1. 先建安全网再动手:项目零测试,先写 tools/validate_scripts.gd 全量编译校验(57 个脚本),作为改动前后的回归基线。没有这一步就不该改核心战斗文件。
  2. 从最容易藏 bug 的文件下手enemy.gd 1479 行、wave_manager.gd 366 行是状态机最复杂的地方;v0.7.0 刚改过的经济系统(weapon.gd)是回归风险最高的地方。这三处果然全中。
  3. 每个 fix 必须有独立证据:① 用运行时探针 + 改前/改后对照(9 个事件 → 1 个事件);③ 用常量核算(pellet_count=8 × ammo_save_chance=0.25 = 期望回补 2.0);⑤ 用参数求和(1.0 + 0.55 > 1.0)。
  4. 诚实边界前置:B站动态里明确写了「只跑了 headless 校验,没真机试玩」「Windows 包还没传」。这些不是在谦虚,是在避免读者基于错误前提做决策(比如下载不存在的新版本)。
  5. 构建产物与发布文案分开存档:三渠道(GitHub / itch / B站)文案各存一份在 docs/release_notes/v0.7.1.md,B站那条又经 Ultracode 工作流单独打磨。

二、工具链真正帮到的环节

1. 脚本编译校验器

--editor --quit 只解析被场景引用到的脚本,孤立文件的语法错误抓不到;--check-only 阶段 autoload 尚未注册,57 个文件里 21 个误报 Identifier not found。最终方案是在完整启动的引擎里逐个 load() + reload()

踩到的坑:编译失败的脚本 ResourceLoader.load 仍返回非 null 对象,只判空会把所有文件都报成健康,必须看 reload() 的返回码。

2. Ultracode 工作流写 B站动态

3 个角度(沿用旧格式 / 「剩 0 只」开场钩子 / 开发者复盘)并行起草,每稿由 3 个独立视角(B站观众 / 事实核查 / 作者语气)评分。结果:story 与 devlog 并列 7.7 分,convention 7.0 分。以 story 为骨架合成,事实核查视角砍掉 9 处失真表述(「上线这么久」→ 项目才两三个月;「清光场上敌人」→ 逻辑自毁;「把上一版经济改造抵消了」→ 只在抽到卡且用霰弹枪时成立)。

这延伸了 子代理并行批量生成与校验组装_v1 的核心:并行单元应互斥,最终组装与验收必须集中。

3. 导出模板的安装

Godot 4.7.1 只装了 Web 模板(上次为 Toy 平台下的),Windows 模板要从 GitHub Release 下载。.tpz 本质是 zip 但扩展名不同,Expand-Archive 不认;后台下载进程锁住文件时,FileShare.ReadWrite 也绕不过。最终先让下载跑完(约 1.2GB),再用 .NET ZipFile::ExtractToDirectory 解压,解压后注意模板在 templates/ 子目录里,要移到 4.7.1.stable/ 根下 Godot 才认。

三、阻塞与恢复

阻塞事实原因恢复方式沉淀
项目零测试,改核心战斗逻辑无安全网没有人手写的单元/集成测试先写 tools/validate_scripts.gd 全量编译校验,57 个脚本作为回归基线无测试的项目,工具补置信度;校验器本身要故意插一个语法错误验证它真的会报
--check-only 报 21 个假阳性该阶段 autoload 未注册改为在完整引擎内 load() + reload()两种 CLI 校验方式都有盲区,不能单独依赖
ResourceLoader.load 对坏脚本返回非 nullGodot 设计:编译失败仍返回 GDScript 对象reload() 返回码判断,不用 null 检查校验脚本编译状态必须看 reload() 返回码
Windows 导出模板没装全4.7.1 只装了 Web 模板从 GitHub Release 下载 1.2GB tpz,解压后移到正确目录发布前检查模板完整性,别等 Export 失败了才装
B站动态初稿失真多个说法超出事实边界Ultracode 工作流 + 独立事实核查视角砍掉 9 处对外发布内容必须过独立事实核查,作者自审会漏
后期敌人分布改了但没验收作者还没打过第 25 波之后B站动态先标「未验证」,等朋友试玩反馈平衡调整必须真机验收,不能只靠逻辑推导

四、可复用方法

方法 1:剩余数 ≠ 已落地数

凡是「生成需要时间」的波次/队列系统,剩余数必须由「计划总数 - 已消耗」推导,不能直接取「当前已存在实例数」。 Godot 里 spawn + 协程 await 是典型场景:循环不等协程结束就推进状态,导致「系统认为的剩余」和「玩家看到的剩余」不一样。

方法 2:按弹丸结算 vs 按枪结算

多张「单次开火多个子单元」的设计(散弹、多弹道、连射),所有「概率触发」效果必须在整枪层级 roll 一次。如果在子单元层级 roll,期望值 = 子单元数 × 概率,平衡直接崩。这次 ammo_save_chance 在霰弹枪上就是 8 倍生效。

方法 3:异步操作的代次校验

凡是「可取消的延迟操作」(换弹、施法、定时技能),取消时要同时作废已排下的回调。 Godot 里 create_timer 不会自动取消,必须手写 token / 代次校验。模式:每次启动自增 token,回调只认自己那一代。

方法 4:概率上限之和必须 < 1.0

用一次 randf() 分区间抽类型时,所有类型的上限之和必须严格小于 1.0。否则最「晚」的类型会彻底拿不出区间,同时倒数第二类会被静默截断。这次 brute 上限 1.0 + runner 上限 0.55 = 1.55,grunt 直接消失。

方法 5:诚实边界三问

发布前自问三个问题,答不清楚就不写:

五、上帝视角:这一轮的整体图景

v0.7.1 是一个特殊周期下的产出:作者经医生同意停药、状态稳定,经复诊确认可控,在 vibe coding 启动独立游戏后进入一个持续性高精力周期。这一版没有加任何新东西,但五个 bug 全藏得深——不是因为代码写得差,而是因为状态机的边界语义容易被误解(剩余 vs 落地、整枪 vs 弹丸、当前换弹 vs 被取消的换弹)。

它同时是一次完整的发布链路演练:代码审读 → 修复 → headless 校验 → 构建 → GitHub Release → B站动态 → 知识库复盘。这条链路从 5 月项目启动以来第一次完整跑通,下一版(v0.7.2,计划:玩家反馈驱动的经济/难度调优 + 武器 2D 持枪视图 + HUD 重绘)会在同一套安全网下迭代。

关联文档

类型/协作工具链工具/Godot工具/Claude Code主题/独立游戏开发主题/纯修复版发布来源/LastStand