| 维度 | 案例 | AI 价值(重写) |
|---|---|---|
| Web/Desktop 双端存档 | WebSaveAdapter deep swap + mirror seeding | 高——人类踩的坑:写死 res:// 路径,Web 端 0 写入 |
| 并发 race condition | BGM session id、page_gift await input 边沿 | 高——人类写协程用全局布尔标志 = 必踩 race |
| 状态机边界管理 | cutscene_trigger 0~8 全套切换 | 中高——AI 强项:把状态空间画完整再写代码 |
| 持久化数据迁移 | axe_count→tool_count、cell_to_inventory_id、tile 复活 | 高——人类漏一个字段迁移 = 老存档全废 |
| 复杂 bug 根因分析 | floor 模型陷阱、tellable TileSet 绕开 | 高——AI 强项:诊断”模型设计缺陷” |
| 跨子系统重构 | Breakable 通用抽象替代特例方法 | 中——人类能做但容易漏特例 |
详细说明:每个维度 AI 怎么做到的
1. Web/Desktop 双端存档 — 高
人类怎么写错的:
- 存档路径写死
res://xxx.tres,Web 端 OS.file system 根本写不进去,console 一片红 - Resource 子属性(如 BagResource 里的 Inventory 子对象)只换了外层,bag_item.current 引用还指向旧 bag → 玩家 transfer 物品,刷新页面物品回去
AI 的做法(三步走):
- 抽象”运行时资源路径”层:
WebSaveAdapter.runtime_path(original_path)把res://xxx.tres翻译成user://runtime_state/<slot>/<basename>,上层代码不用关心平台 - cold-start 镜像机制:
SEED_RUNTIME_PATHS列 31 个核心 .tres,cold-start 拷贝到 user://,只 seed 一次——后续操作全在镜像版上做 - deep swap:
swap_to_runtime_deep()递归把 Resource 子属性切到镜像版;然后主动补_relink_inventory_after_swap()把 BagItem/HoldItem.current 字典引用按 inventory.id 重新指向新 bag
关键洞察:人类写存档时不会主动思考”子对象引用还在原对象”这种二阶问题——AI 会因为见过 deep copy / shallow copy 的坑,主动 grep 所有引用点统一迁移。
2. 并发 race condition — 高
人类怎么写错的:
- 用
is_back_audio_running = true当协程退出条件 → 旧协程await timer30 秒醒来时,新协程已经把is_back_audio_running重设 True → 旧协程继续跑 → 两个协程抢同一个back_audio_player→ BGM 一首没播完就硬切 await Input.is_action_just_pressed("left_mouse")在tween.finished回调里 → 玩家刚才点的 left_mouse.just_pressed 边沿还没被消费 → 协程第一帧就命中 → 立刻关掉后续 tips
AI 的做法(两个暗坑分别破):
- session id 替代布尔标志:每次
play_level_audio_by_name/pause_bgm_immediately自增_bgm_session_id;协程每次 await 后检查if _bgm_session_id != session_id: return退出。关键洞见:Godot 4 的AudioStreamPlayer.stop()不 emit finished(不像 play()),所以协程await finished永远不会被唤醒——必须靠 session id 兜底 await process_frame吞边沿:在 await Input 之前先await get_tree().process_frame,让那一帧的 just_pressed 边沿在 _process 阶段被消费掉,下一帧再开始监听
关键洞察:人类写协程时不会预演”两个协程抢同一资源”的 timeline——AI 善于画时序图,先识别状态机边界再写守卫。
3. 状态机边界管理 — 中高
人类怎么写错的:
- sprint 门禁写成
if cutscene_trigger == 7: allow_sprint()——只在 trigger 7 允许跑步,但剧情流是 trigger 0~8 共 9 个状态,trigger 8/未来 9/10 全部被卡死 - 状态转移条件漏写:trigger 5→6 转换只写了 “pick 按下触发”,没考虑 “sleep area body_entered + Sleep 动画播完 + 日期+1 + 7:00 reset + load_level” 全部都得按顺序完成
AI 的做法:
- 先画完整状态转移表:trigger 0~8 共 9 个状态,每个转移的 owner(在哪个文件)+ 时机(什么时候触发)+ 副作用(emit 什么、改什么字段)
- 用 owner map 表格化:8 个 trigger 转移 → 8 行表,把所有触发点列全。后续任何 trigger 改动先查这张表
- 门禁反推:从”哪些 trigger 该允许 sprint”反推出门禁条件是
cutscene_trigger > 6(不是== 7)——AI 不会把门禁写死成单值
关键洞察:人类写状态机习惯按当前需求写当前条件——AI 强项是把整个状态空间列全,画清楚每个边界。
4. 持久化数据迁移 — 高
人类怎么写错的:
- 改 schema 时直接 rename
axe_count→tool_count,老存档加载时 push_error → 所有砍树进度归零 - 加新字段时不写默认值,老存档没字段就崩
AI 的做法(三大套路):
- 字段迁移(_ready 守卫):
if current.has("axe_count") and not current.has("tool_count"): current["tool_count"] = current["axe_count"]; current.erase("axe_count")——运行时一次性迁移 - 反查表(hard-coded const):
_INVENTORY_ID_TO_FILENAME92 项,不靠自动扫目录——硬编码确保 reload 时不会漏。漏登记 → push_warning + skip → 表现是”新放能砍、存档重载砍不动” - 持久化视图与内存视图同构:
BuildingResource.tellable_broken_cells↔Level.tellable_broken_cells用同一个 dict 结构,reload 时按 layer_key 反向erase_cell还原
关键洞察:人类改 .tres schema 时只想着新代码怎么写——AI 强项是先想”老存档加载时会发生什么”,每个字段都加兼容守卫。
5. 复杂 bug 根因分析 — 高
人类怎么诊断错的:
- 看到”吃东西没反应”会调 console 查报错,但 floor 模型陷阱不报错——玩家视觉看到能量条 12.5% 觉得”快没电”,右键 pumpkin 却被守卫拦掉(物品不扣,电流不动)
- TileSet
farmland=true标记的 cell 才能锄——但 Afforest 装饰 tile 不是 farmland。人类会去改 TileSet(侵入美术资产),改完整个 outdoor 场景的 tile 都要重新画
AI 的诊断法(两步破):
floor 模型陷阱:
- 列出所有边界条件组合:
(floor=0, current=0)/(floor=0, current=ceiling)/(floor=35, current=5)/(floor=40, current=0)(累瘫) - 逐个跑守卫
if current >= ceiling: return看哪个失败 → 发现 floor=35/current=5 失败 - 改成”预测算法”:
future_current = min(future_ceiling, current + delta),if future_current <= current: return——能涨就吃
TileSet 绕开:
- 不去改 TileSet(侵入美术)
- 引入新机制外层绕开:metadata/tellable + BreakableTellable + tellable_broken_cells 持久化
get_current_tile_can_hoe第一优先遍历 tellable 层,绕过 TileSet 自定义数据直接判定
关键洞察:AI 强项不是”看代码”——是先列 invariant(不变量),再列守卫,最后列边界。人类看 bug 看症状,AI 看 bug 看模型。
6. 跨子系统重构 — 中
人类怎么写错的:
- axe 砍 fence 走
axe_fence(),砍建筑走can_axe_fence,砍宝箱走 chest 自己的break()——每个特例要在 Level 加方法、一段 if-else、一份配置表 - 加新可砍物品要改 4~5 个文件
AI 的做法(三条铁律):
- 配置表集中化:3 张 const 全在
Level顶部 200 行——BREAKABLE_SCRIPT_BY_INVENTORY_ID(4 项特例)、BREAKABLE_HITS_NEEDED_BY_INVENTORY_ID(3 项特例)、_INVENTORY_ID_TO_FILENAME(92 项)。加新特例改表不动资源 - 保留旧接口为 deprecated alias:
axe_fence/to_axe/fence_axe_count都保留只为 “unknown method” 误报兜底——不是为了兼容,是为了 grep 时能找到所有旧用法 - 接口名对齐抽象意图:
to_axe()→to_tool()而非保留to_axe——因为未来 hammer / pickaxe 不该叫to_hammer(),应该统一叫to_tool()参数化
关键洞察:人类重构时图省事保留旧接口——AI 强项是坚持”接口名必须反映抽象意图”,即使保留 deprecated 也要把主接口名改对。
横向洞察:AI 在这 6 个维度的共同特质
| AI 特质 | 在哪个维度表现最明显 |
|---|---|
| 画时序图 / 状态图再写代码 | 并发 race、状态机边界 |
| 列 invariant 再写守卫 | 持久化迁移、bug 根因 |
| 配置表集中化 | 跨子系统重构、双端存档 |
| 保留 deprecated + 改主接口名 | 跨子系统重构、接口契约 |
| 先想”老代码/老存档会怎样” | 持久化迁移、双端存档 |
| 绕过而非侵入 | bug 根因(TileSet 绕开)、重构(hold_effect 抽象) |
一句话总结:AI 在这个项目里不是”写得快”,是”先想全再动手”——状态机先画完、invariant 先列完、老代码先扫完,才动手写。这样写出来的代码,每个守卫、每个反查表、每个 deprecated alias 都不是事后补丁,是设计意图的直接体现。