From 2a7658e0e38f3fbe3bcc715b181a7adf69d45a61 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 23 Jul 2026 16:35:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(hot-path):=20=E5=9B=9E=E9=80=80=20query=5Fc?= =?UTF-8?q?ircle=20=E5=A4=8D=E7=94=A8=E7=BC=93=E5=86=B2=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=AD=98=E5=9C=A8=E9=87=8D=E5=85=A5=E8=A6=86?= =?UTF-8?q?=E5=86=99=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上个提交 35c1742 让 query_circle 复用成员 _query_result 缓冲,实测有重入缺陷: GDScript 的 PackedArray 返回值是活引用别名(非写时复制副本),当调用方遍历返回 的 hits 期间触发嵌套 query_circle(命中→SpellEvaluator.execute_sub 的区域法术), 嵌套调用的 clear()+append 会就地覆写外层正在遍历的同一缓冲,导致碰撞遍历读到错误 实体 ID。编辑器内已复现:外层期望 [10,20,30],重入后实得 [10,88]。 原优化仅省约 0.064ms/帧(帧预算 ~0.4%),不值得为其做跨 4 处调用点的 out 参数 + 各自持久缓冲的重入安全改造,故回退为每次新建数组。 BulletManager 无冷数据命中快路径无重入问题,保留。 并加注释说明此处禁止复用缓冲的原因。 Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/autoloads/spatial_grid.gd | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scripts/autoloads/spatial_grid.gd b/scripts/autoloads/spatial_grid.gd index fe2230a..c802941 100644 --- a/scripts/autoloads/spatial_grid.gd +++ b/scripts/autoloads/spatial_grid.gd @@ -14,12 +14,6 @@ var _cs_node: Node = null # SpatialGridCs C# 子节点(就绪后挂载) var _grid: Array = [] # Array[Array[int]] var _entity_cell: Dictionary = {} # { entity_id → cell_idx } -## query_circle 复用结果缓冲(避免每次调用新建 PackedInt32Array) -## 由 clear()+append 就地填充后返回;调用方按值(CoW)读取。 -## 若调用方在遍历返回值时触发嵌套 query_circle(如命中→SpellEvaluator 区域查询), -## 下一次 clear() 因引用计数>1 触发写时复制,旧结果不被破坏,语义与新建数组等价。 -var _query_result: PackedInt32Array = PackedInt32Array() - func _ready() -> void: _grid.resize(GRID_COLS * GRID_ROWS) for i in _grid.size(): @@ -45,7 +39,11 @@ func insert(entity_id: int, pos: Vector2) -> void: func query_circle(center: Vector2, radius: float) -> PackedInt32Array: if _cs_node and _cs_node.has_method("QueryCircle"): return _cs_node.call("QueryCircle", center, radius) - _query_result.clear() + # 注意:必须返回新建数组,不可复用成员缓冲。 + # 调用方(BulletManager._check_collision)在遍历返回值期间可能触发嵌套 query_circle + # (命中→SpellEvaluator.execute_sub 的区域法术),GDScript 的 PackedArray 返回值是活引用别名, + # 复用成员缓冲会被嵌套调用就地覆写、破坏外层遍历(已实测复现)。 + var result := PackedInt32Array() var min_cx: int = clamp(int((center.x - radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_COLS - 1) var max_cx: int = clamp(int((center.x + radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_COLS - 1) var min_cy: int = clamp(int((center.y - radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_ROWS - 1) @@ -53,8 +51,8 @@ func query_circle(center: Vector2, radius: float) -> PackedInt32Array: for cy in range(min_cy, max_cy + 1): for cx in range(min_cx, max_cx + 1): for eid in _grid[cx + cy * GRID_COLS]: - _query_result.append(eid) - return _query_result + result.append(eid) + return result ## 每帧由 EnemyManager._physics_process 驱动(dirty-list 方案) func rebuild() -> void: