上个提交 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>
80 lines
3.2 KiB
GDScript
80 lines
3.2 KiB
GDScript
## SpatialGrid — 2D 空间哈希网格 Autoload(GDScript 外壳)
|
||
## 热路径由 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()
|