发布于 更新于 约 8 分钟读完 · 2,792 字

本文目录 16 节
  1. 事实记录(不可修改区)
  2. 一句话结论
  3. 一、实际走通的链路
  4. 二、工具链真正帮到的环节
  5. 1. 脚本编译校验器
  6. 2. Ultracode 工作流写 B站动态
  7. 3. 导出模板的安装
  8. 三、阻塞与恢复
  9. 四、可复用方法
  10. 方法 1:剩余数 ≠ 已落地数
  11. 方法 2:按弹丸结算 vs 按枪结算
  12. 方法 3:异步操作的代次校验
  13. 方法 4:概率上限之和必须 < 1.0
  14. 方法 5:诚实边界三问
  15. 五、上帝视角:这一轮的整体图景
  16. 关联文档

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 波之后验收

事实记录(不可修改区)

  • 起点:作者经医生建议停药精神科药物已一年半、状态稳定,经两周前复诊确认可控;四个多月前接触 vibe coding 后启动 LastStand 开发,状态出现持续性变化(睡眠需求减少、精力充沛)。本次 v0.7.1 是这一周期下的产出。
  • 版本性质:纯修复版,没有新内容。从 v0.7.0(2026-06 发布,经济改造 + 步态动效 + 实测批修)升级而来。
  • 五个修复全部来自代码审读:没有一条是玩家报的。作者逐文件读代码时发现。
  • 修复 ① 波次剩余数显示错误:每波开局 HUD 先显示「剩 0 只」,再花约 0.8 秒从 0 数到满。根因:_alive_enemies.size() 在生成阶段取的是「已落地数」,而敌人要等各自的 spawn_effect_duration(0.8 秒,boss/elite 1.28 秒)生成烟播完才 append。改法:新增 _wave_total 与 _wave_killed,剩余数 = 总数 - 已击杀。对照实验坐实:改前 9 个事件(0→1→2→…→7),改后单个 remaining=7。
  • 修复 ② 提前清波(竞态):_spawn_at 是协程但不被 await,生成循环只等 spawn_stagger(0.1 秒/只)就置 COMBAT。此时最后几只还没落地。这段窗口(最长 0.8 秒)内清光已落地的敌人会提前通关、弹升级面板,剩余敌人生成到下一波。改法:新增 _spawns_pending 计数,清波判定前检查是否还有未落地的敌人。
  • 修复 ③ 弹药节省在霰弹枪上多倍生效:传说卡,卡面「命中时 25% 概率不消耗弹药」。原回补写在 _fire_pellet 内,该函数按弹丸调用;霰弹枪 pellet_count = 8,一枪扣 1 发却 roll 8 次,8 颗全中时期望回补 2.0 发——弹匣越打越满,直接抵消 v0.7.0 刚做的货币稀缺化。改法:移到 try_shoot 整枪只 roll 一次。
  • 修复 ④ 切枪打断换弹后的计时器串扰:on_unequipped 清 is_reloading,但已排下的 create_timer 仍会到点。切回来重新换弹时,旧回调会把新的那次提前结算。改法:加 _reload_token 代次校验,回调只认自己那一代。
  • 修复 ⑤ 后期 grunt 绝迹:敌种抽取用一次 randf() 分区间,brute 与 runner 概率各自随波次增长、上限之和(1.0 + 0.55 = 1.55)超过 1.0,第 21–23 波起 grunt 拿不到任何区间,同时 runner 被静默截断到 40%。改法:brute_chance_max 从 1.0 收到 0.30,后期稳定为 brute 30% / runner 55% / grunt 15%。附带影响:grunt 专属的包抄 AI(极限档限定)不再从最后十波消失。
  • 内部改动:敌人互推 / 告警 / 同步攻击三处原本每帧各自调 get_nodes_in_group("enemy"),改为按物理帧号缓存一次、全场共用;补齐互推里缺失的 is Enemy 类型守卫;README 引擎版本从 4.6 更正到 4.7(项目实际已是 4.7);新增 tools/validate_scripts.gd 脚本编译校验工具。
  • 发布流程:GitHub Release 自动构建 Windows 包 → 上传 Release 附件;B站动态经 Ultracode 工作流(3 角度起草 × 3 视角评审 → 合成定稿)产出;itch.io devlog 与 GitHub Release 文案另存。
  • 未验证:没有真机试玩;第 5 条改了后期敌人分布,作者还没打过第 25 波之后;没有玩家反馈回收。

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

一句话结论

这一版的核心教训是:没有测试套件的项目,修复的置信度靠工具补;五个 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