Files
spellforge/scripts/autoloads/spatial_grid.gd
T
joywayerandClaude Opus 4.8 2a7658e0e3 fix(hot-path): 回退 query_circle 复用缓冲优化——存在重入覆写 bug
上个提交 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) <noreply@anthropic.com>
2026-07-23 16:35:31 +08:00

80 lines
3.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## SpatialGrid — 2D 空间哈希网格 AutoloadGDScript 外壳)
## 热路径由 C# SpatialGridCs 子节点执行(就绪后接管)
## 权威来源:architecture_design.md §4.3
extends Node
const CELL_SIZE: int = 64 # 格子大小(像素)
const GRID_COLS: int = 128 # 128×128 格 × 64px = 8192×8192 覆盖范围
const GRID_ROWS: int = 128
const GRID_OFFSET: int = 4096 # 将世界中心映射到网格中心(支持负坐标)
var _cs_node: Node = null # SpatialGridCs C# 子节点(就绪后挂载)
## GDScript 回退网格
var _grid: Array = [] # Array[Array[int]]
var _entity_cell: Dictionary = {} # { entity_id → cell_idx }
func _ready() -> void:
_grid.resize(GRID_COLS * GRID_ROWS)
for i in _grid.size():
_grid[i] = []
## 插入或更新实体位置(由 EnemyManager 每物理帧调用)
func insert(entity_id: int, pos: Vector2) -> void:
if _cs_node and _cs_node.has_method("Insert"):
_cs_node.call("Insert", entity_id, pos)
return
var cx: int = clamp(int((pos.x + GRID_OFFSET) / CELL_SIZE), 0, GRID_COLS - 1)
var cy: int = clamp(int((pos.y + GRID_OFFSET) / CELL_SIZE), 0, GRID_ROWS - 1)
var cell: int = cx + cy * GRID_COLS
var old_cell: int = _entity_cell.get(entity_id, -1)
if old_cell == cell:
return
if old_cell >= 0:
_grid[old_cell].erase(entity_id)
_grid[cell].append(entity_id)
_entity_cell[entity_id] = cell
## 查询圆形范围内的所有实体 ID
func query_circle(center: Vector2, radius: float) -> PackedInt32Array:
if _cs_node and _cs_node.has_method("QueryCircle"):
return _cs_node.call("QueryCircle", center, radius)
# 注意:必须返回新建数组,不可复用成员缓冲。
# 调用方(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)
var max_cy: int = clamp(int((center.y + radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_ROWS - 1)
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]:
result.append(eid)
return result
## 每帧由 EnemyManager._physics_process 驱动(dirty-list 方案)
func rebuild() -> void:
if _cs_node and _cs_node.has_method("Rebuild"):
_cs_node.call("Rebuild")
## 移除实体
func remove_entity(entity_id: int) -> void:
if _cs_node and _cs_node.has_method("Remove"):
_cs_node.call("Remove", entity_id)
return
var old_cell: int = _entity_cell.get(entity_id, -1)
if old_cell >= 0:
_grid[old_cell].erase(entity_id)
_entity_cell.erase(entity_id)
## 清空(关卡重置)
func clear() -> void:
if _cs_node and _cs_node.has_method("Clear"):
_cs_node.call("Clear")
return
for cell_list in _grid:
(cell_list as Array).clear()
_entity_cell.clear()