From 1215b08e88a00059d333704f06add611e3294045 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 23 Jul 2026 16:38:55 +0800 Subject: [PATCH] =?UTF-8?q?docs(dev):=20=E6=96=B0=E5=A2=9E=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E8=BF=81=E7=A7=BB=E7=AC=94=E8=AE=B0=E2=80=94=E2=80=94?= =?UTF-8?q?GDScript=20=E5=88=AB=E5=90=8D=E9=99=B7=E9=98=B1=20+=20C#/C++=20?= =?UTF-8?q?=E6=BF=80=E6=B4=BB=E6=9C=9F=E6=8E=A5=E7=BC=9D=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E6=80=A7=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 沉淀两类内容并登记 docs_dev 索引: ① GDScript 陷阱:PackedArray 返回值是活引用别名(非 CoW 副本),复用成员缓冲后 return 会被调用方遍历期间的嵌套调用就地覆写(query_circle 复用优化翻车、已回退的教训), 附实测证据 + 通用规则(默认每次新建;省分配用 out 参数+各调用点独立缓冲)。 ② C#/C++ 激活期接缝一致性项:SpatialGridCs 网格常量已对齐(已修); EnemyManager 原型字典 _SPEED/_ARMOR 内循环逐敌查,迁移期须在 C# 侧从 enemies.json 自建原生数组以守 ADR-L1 铁律(a),现在不改(避免投机 churn)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- docs_dev/2026-07-23-native-migration-notes.md | 95 +++++++++++++++++++ docs_dev/README.md | 1 + 2 files changed, 96 insertions(+) create mode 100644 docs_dev/2026-07-23-native-migration-notes.md diff --git a/docs_dev/2026-07-23-native-migration-notes.md b/docs_dev/2026-07-23-native-migration-notes.md new file mode 100644 index 0000000..2e7a2f6 --- /dev/null +++ b/docs_dev/2026-07-23-native-migration-notes.md @@ -0,0 +1,95 @@ +# 原生迁移笔记 —— GDScript 陷阱与 C#/C++ 激活期注意项(2026-07-23) + +> 类型:笔记 / 迁移备忘 +> +> 背景:本工程运行时 100% 为 GDScript,C#/C++ 为「就绪但休眠」的逃生舱(见 +> [`../CLAUDE.md`](../CLAUDE.md) 编程语言规范、`R-08` / `ADR-L1`)。实测 500 敌 + 1500 弹 +> `_physics_process` ≈ 0.6ms(帧预算 ~3.6%),性能充裕,**当前无任何热路径够格迁移**。 +> 本文沉淀两类内容:① 一次热路径「零成本优化」翻车暴露的 GDScript 陷阱与通用规则; +> ② 将来真按 `ADR-L1` 激活 C#/C++ 内核时必须处理的接缝一致性项。 + +--- + +## 1. GDScript 陷阱:PackedArray 返回值是「活引用别名」,不可复用成员缓冲后返回 + +### 事件 +为削减 `SpatialGrid.query_circle`(每帧被 BulletManager 调用 1500+ 次)每次新建 +`PackedInt32Array` 的开销,曾将其改为复用一个成员缓冲 `_query_result`(`clear()`+`append` +就地填充后 `return`),editor 基准显示约 **11% 提速**、2000 次**顺序**查询结果与原实现逐点一致。 +**但该改动有重入(reentrancy)缺陷,已回退**(提交 `35c1742` 引入 → `2a7658e` 回退)。 + +### 根因 +GDScript 中 `Array` / `Dictionary` / `Packed*Array` **按引用传递**,且**函数返回的 PackedArray +是底层缓冲的活引用别名,而非写时复制(CoW)副本**。因此当调用方仍在遍历返回的 +`hits` 时,若中途触发一次**嵌套** `query_circle`(真实路径:子弹命中 → +`SpellEvaluator.execute_sub` 的区域法术再查一次网格),嵌套调用的 `clear()`+`append` +会**就地覆写外层正在遍历的同一缓冲**,导致碰撞遍历读到错误的 entity_id(打错敌人 / 漏敌)。 + +### 实测证据(editor 内复现) +- out 参数确会传回:`fill(out_arr)` 内 `resize`+写入后,调用方变量 size/值均更新(证伪「CoW 会分叉」的误判)。 +- 重入覆写复现:外层缓冲填 `[10,20,30]` 开始遍历,遍历中嵌套查询填 `[77,88]`, + 外层实际遍历到 **`[10,88]`**(而非期望的 `[10,20,30]`)。 + +### 通用规则(写 GDScript 热路径时遵守) +1. **返回 PackedArray 的函数,若调用方可能在遍历返回值期间重入本函数,绝不可复用成员缓冲后 `return`**—— + 返回值是活引用别名,会被重入覆写。默认每次 `return` 新建数组是安全写法。 +2. 想省分配又要安全,用 **out 参数 + 每个调用点各自持有独立持久缓冲** + (`func query_into(center, radius, out: PackedInt32Array)`,out 会正确传回)。 + 前提:须逐调用点确认该缓冲不会自我重入。`query_circle` 有 4 处调用点 + (BulletManager / ZoneManager / MinionManager / SpellEvaluator),改造面大。 +3. **只有实测收益足够大才值得**。本例仅省约 **0.064ms/帧(帧预算 ~0.4%)**, + 不足以支撑跨 4 调用点的重入安全改造,故回退——符合工程「先测量后优化、无凭据不下探」原则。 + +### 元教训 +「顺序调用逐点一致」**不等于**正确;**没覆盖到的路径(此处是重入)就是没验证**。 +热路径改动务必显式设计并验证重入 / 别名场景。 + +--- + +## 2. C#/C++ 激活期接缝一致性项(`ADR-L1` 触发时必查) + +真按 `ADR-L1` 激活原生内核前,务必核对休眠骨架与 GDScript 权威外壳的**接缝契约**逐一对齐, +否则「激活即塌」。当前已知项: + +### 2.1【已修】SpatialGridCs 网格常量 / 坐标偏移 +`csharp/systems/SpatialGridCs.cs` 曾与权威外壳 `scripts/autoloads/spatial_grid.gd` 三处不一致, +2026-07-23 已对齐(提交 `7038505`,`dotnet build` 0 错误): + +| 项 | 权威外壳 `spatial_grid.gd` | 修前 `SpatialGridCs.cs` | +| :-- | :-- | :-- | +| 网格尺寸 | `GRID_COLS/ROWS = 128`(8192² 覆盖) | ~~64×64(4096²)~~ → 128×128 | +| 坐标偏移 | `GRID_OFFSET = 4096`(支持负坐标) | ~~无偏移~~ → 补 `GridOffset=4096` | +| 格映射 | `(coord ± radius + 4096) / 64` | ~~`coord/64`(负坐标塌到 0 格)~~ → 已加偏移 | + +> C# 侧已加注释「必须与 spatial_grid.gd 常量逐一对齐」。今后改任一侧常量须同步另一侧。 + +### 2.2【待办·迁移期】EnemyManager 原型数值为 GDScript 字典,内循环逐敌查 +`scripts/autoloads/enemy_manager.gd` 的 `_SPEED / _ARMOR / _RENDER_SIZE / _RENDER_COLOR` +是**按 `enemy_type` 键的 GDScript `Dictionary`**(`enemy_manager.gd:40-43`), +在 `_gd_update_movement` 内循环 `_SPEED.get(int(_data[base+6]), ...)` **逐敌查字典** +(`:110`,`apply_damage` 的 `_ARMOR.get` 同理 `:268`)。 + +- **现状(纯 GDScript)无问题**:字典查在 GD 内是本地操作,无跨语言封送。 +- **迁移期风险**:一旦 EnemyManagerCs 接管内循环,若在 C# 内层 `Get("_SPEED")...` 逐敌读该字典, + 即违反 `ADR-L1` 铁律 (a)「内循环禁止 `GodotObject.Call()/.Get()/.Set()`」(每次约 1–5µs 封送, + ×上千敌/帧即耗尽帧预算)。 +- **迁移期做法**:C# 内核启动时从**同一份** `res://data/enemies.json` 自建 + `type → speed/armor/...` 原生数组(`int[]` / `float[]`),内循环只读原生数组, + 不回调 GDScript 字典。属纯数据驱动、无内容副本,与现有加载逻辑 `_load_json_archetypes()` 同源。 +- **注意**:此项**现在不改**——在纯 GD 期把它改成别的结构属无数据背书的投机 churn。 + 仅当真触发 C# 迁移时按上述做法在 C# 侧落地。 + +### 2.3 其余接缝(已核对,就绪) +- BulletManager / EnemyManager 热数据均为 SoA `PackedFloat32Array _data` 成员, + 步长为命名常量(`BULLET_STRIDE=12` / `ENEMY_STRIDE=8`)并有布局注释,C# 可 `AsSpan()` 零拷贝直读。 +- 三处外壳均有 `_cs_node` 接缝 + 干净 GD 回退(`if not _cs_node:`),骨架未加载时游戏照常运行。 +- BulletManagerCs / EnemyManagerCs 现为 S0 骨架,仅读 `_active_count` 打帧计时, + 不含 SoA 布局常量,无对齐问题。 + +--- + +## 参考 +- 语言规范 / 三级逃生梯:[`../CLAUDE.md`](../CLAUDE.md) +- 热路径边界规则(`ADR-L1` 三铁律 + `R-08`):`../docs/technical/architecture_design.md` §6.1 +- S0 预算与实测:`../docs/technical/architecture_design.md` §(S0 预算表,~786–808 行) +- 文档 vs 代码审计(C# 死代码现状):[`doc_code_audit_2026-07-20.md`](doc_code_audit_2026-07-20.md) diff --git a/docs_dev/README.md b/docs_dev/README.md index ce4b36f..8f9b8e9 100644 --- a/docs_dev/README.md +++ b/docs_dev/README.md @@ -7,6 +7,7 @@ | 文档 | 类型 | 内容简述 | | :--- | :--- | :--- | | [doc_code_audit_2026-07-20.md](doc_code_audit_2026-07-20.md) | 审计 | 文档 vs 代码交叉审计:三大结构性分歧、更优/偏离/未实现分类清单、潜在 bug、文档内部矛盾(2026-07-20) | +| [2026-07-23-native-migration-notes.md](2026-07-23-native-migration-notes.md) | 笔记 / 迁移备忘 | ① GDScript 陷阱:PackedArray 返回值是活引用别名,复用缓冲会被重入覆写(query_circle 优化翻车教训);② C#/C++ 激活期接缝一致性项:SpatialGridCs 常量对齐(已修)、EnemyManager 原型字典需 C# 侧自建原生数组(待办·迁移期) | | [development_plan.md](development_plan.md) | 计划 / 追踪 | S0–S6 骨架垂直切片开发计划、技术风险登记表、各切片验收与出口检查、`P-S*` 任务勾选 | | [certification_checklist.md](certification_checklist.md) | 计划 / 追踪 | Steam / Nintendo Switch 平台发行认证清单,映射到对应切片,含状态列 | | [archived_cocos_architecture_draft.md](archived_cocos_architecture_draft.md) | 归档(已废弃) | 早期基于 Cocos Creator 3.x / TypeScript 的架构草案。项目已迁移至 Godot 4.6,权威架构见 [`../docs/technical/architecture_design.md`](../docs/technical/architecture_design.md) |