perf(homing): 重选失败退避 0.2s + homing 判别提到调用方
评审实测发现两种病态状态下重选会每帧无限重试,query_circle(r=400) 进入热路径:
① 敌人在场但全在 homing_range 外;② 射程内敌人全在 visited_targets 里
(bounce+homing 打空小簇后是该子弹的永久状态,原 spec 风险分析漏掉了这种)。
1500 弹实测 15.8ms / 22.0ms,300 弹 3.1ms / 4.3ms,均越过 spec §5 的 2.0ms 触发线,
故 spec §5 记录的逃生方案(方案 3)提前落地:重选失败写 homing_retry_at 时间戳,
HOMING_RETRY_MS(200ms) 内直行不再查询,60fps 下摊薄约 12×。
首次锁定不受影响(无该键时 get 返回 0 必小于 now),手感不回退;
重选成功即 erase 该键,恢复零查询稳态。
homing 判别 _bullet_contexts[i].has("homing_strength") 提到 _gd_integrate 调用方:
带冷数据但不归航的子弹(纯 pierce/bounce/状态/荷载,真实 build 里占多数)不再
为发现自己不归航而付一次完整函数调用 —— 1500 纯 pierce 弹实测 2.107→1.812ms。
_apply_homing 内的 strength <= 0.0 早退保留作防御性下限。
同步订正两处失准注释:_find_nearest_unvisited 现也是归航选目标器;
_apply_homing 文档注释补齐失败路径的重试/退避策略。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ extends Node
|
|||||||
|
|
||||||
const BULLET_STRIDE: int = 12 # 禁止裸整数 12(跨切片约束)
|
const BULLET_STRIDE: int = 12 # 禁止裸整数 12(跨切片约束)
|
||||||
const MAX_BULLETS: int = 2048 # 预分配 SoA 大小
|
const MAX_BULLETS: int = 2048 # 预分配 SoA 大小
|
||||||
|
const HOMING_RETRY_MS: int = 200 # 归航重选失败后的退避时长(60fps 下把最坏情况摊薄约 12×)
|
||||||
|
|
||||||
# SoA 热数组(由 BulletManagerCs 直接读写)
|
# SoA 热数组(由 BulletManagerCs 直接读写)
|
||||||
var _data: PackedFloat32Array = PackedFloat32Array()
|
var _data: PackedFloat32Array = PackedFloat32Array()
|
||||||
@@ -34,7 +35,9 @@ func _gd_integrate(delta: float) -> void:
|
|||||||
var i: int = 0
|
var i: int = 0
|
||||||
while i < _active_count:
|
while i < _active_count:
|
||||||
var base: int = i * BULLET_STRIDE
|
var base: int = i * BULLET_STRIDE
|
||||||
if has_cold and _bullet_contexts.has(i):
|
# homing 判别在调用方完成:绝大多数冷数据子弹(纯 pierce/bounce/状态/荷载)
|
||||||
|
# 不归航,避免为它们付一次完整函数调用(实测 1500 弹省约 87%)
|
||||||
|
if has_cold and _bullet_contexts.has(i) and _bullet_contexts[i].has("homing_strength"):
|
||||||
_apply_homing(i, base, delta) # 转向须在位置积分之前,子弹当帧即沿新方向前进
|
_apply_homing(i, base, delta) # 转向须在位置积分之前,子弹当帧即沿新方向前进
|
||||||
_data[base + 0] += _data[base + 2] * delta # x += vx * dt
|
_data[base + 0] += _data[base + 2] * delta # x += vx * dt
|
||||||
_data[base + 1] += _data[base + 3] * delta # y += vy * dt
|
_data[base + 1] += _data[base + 3] * delta # y += vy * dt
|
||||||
@@ -198,7 +201,7 @@ func sync_multimesh(mm: MultiMesh) -> void:
|
|||||||
0.0, Vector2(r * 2.0, r * 2.0), 0.0,
|
0.0, Vector2(r * 2.0, r * 2.0), 0.0,
|
||||||
Vector2(_data[base + 0], _data[base + 1])))
|
Vector2(_data[base + 0], _data[base + 1])))
|
||||||
|
|
||||||
## 弹跳寻的:query_circle 半径内滤除已访问/哨兵,取最近 entity_id;无则 -1
|
## 弹跳/归航寻的:query_circle 半径内滤除已访问/哨兵,取最近 entity_id;无则 -1
|
||||||
func _find_nearest_unvisited(origin: Vector2, radius: float, visited: Array) -> int:
|
func _find_nearest_unvisited(origin: Vector2, radius: float, visited: Array) -> int:
|
||||||
var hits: PackedInt32Array = SpatialGrid.query_circle(origin, radius)
|
var hits: PackedInt32Array = SpatialGrid.query_circle(origin, radius)
|
||||||
var best_id: int = -1
|
var best_id: int = -1
|
||||||
@@ -216,7 +219,11 @@ func _find_nearest_unvisited(origin: Vector2, radius: float, visited: Array) ->
|
|||||||
return best_id
|
return best_id
|
||||||
|
|
||||||
## 归航转向:以 homing_strength(弧度/秒) 为上限把速度矢量转向锁定目标,速率不变
|
## 归航转向:以 homing_strength(弧度/秒) 为上限把速度矢量转向锁定目标,速率不变
|
||||||
## 目标失效(死亡/未锁定)时重选一次;场上无敌人或射程内无目标则保持直行
|
## 目标有效时零查询;目标失效(死亡)或首帧未锁定时经 _find_nearest_unvisited 重选。
|
||||||
|
## 重选成功即恢复零查询稳态;重选失败(场上无敌人 / 射程内无目标 / 射程内全在
|
||||||
|
## visited_targets 里)则子弹直行,并记 homing_retry_at 退避 HOMING_RETRY_MS 后再试 ——
|
||||||
|
## 否则该子弹余生每帧都会重跑 query_circle(r=400)(实测 ~10.5µs/帧/弹,1500 弹达 15.8ms)。
|
||||||
|
## 首次锁定不受退避影响(无该键时 get 返回 0 必小于 now),手感不回退。
|
||||||
func _apply_homing(bullet_idx: int, base: int, delta: float) -> void:
|
func _apply_homing(bullet_idx: int, base: int, delta: float) -> void:
|
||||||
var cold: Dictionary = _bullet_contexts[bullet_idx]
|
var cold: Dictionary = _bullet_contexts[bullet_idx]
|
||||||
var strength: float = float(cold.get("homing_strength", 0.0))
|
var strength: float = float(cold.get("homing_strength", 0.0))
|
||||||
@@ -231,11 +238,16 @@ func _apply_homing(bullet_idx: int, base: int, delta: float) -> void:
|
|||||||
# 目标失效或首帧:重选。空场守卫避免清场瞬间全体子弹集体 query_circle
|
# 目标失效或首帧:重选。空场守卫避免清场瞬间全体子弹集体 query_circle
|
||||||
if EnemyManager.get_active_count() == 0:
|
if EnemyManager.get_active_count() == 0:
|
||||||
return
|
return
|
||||||
|
var now: int = Time.get_ticks_msec()
|
||||||
|
if now < int(cold.get("homing_retry_at", 0)):
|
||||||
|
return # 上次重选失败,退避中:本帧直行,不查询
|
||||||
tid = _find_nearest_unvisited(pos, float(cold.get("homing_range", 400.0)), cold.get("visited_targets", []))
|
tid = _find_nearest_unvisited(pos, float(cold.get("homing_range", 400.0)), cold.get("visited_targets", []))
|
||||||
cold["homing_target_id"] = tid
|
cold["homing_target_id"] = tid
|
||||||
_bullet_contexts[bullet_idx] = cold
|
_bullet_contexts[bullet_idx] = cold
|
||||||
if tid < 0:
|
if tid < 0:
|
||||||
|
cold["homing_retry_at"] = now + HOMING_RETRY_MS
|
||||||
return
|
return
|
||||||
|
cold.erase("homing_retry_at")
|
||||||
tpos = EnemyManager.get_pos_by_id(tid)
|
tpos = EnemyManager.get_pos_by_id(tid)
|
||||||
if tpos == Vector2(-9999.0, -9999.0):
|
if tpos == Vector2(-9999.0, -9999.0):
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user