From 2f9cb763081ab7aca15acd0117994c2d491eb134 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 30 Jul 2026 16:01:31 +0800 Subject: [PATCH] =?UTF-8?q?perf(homing):=20=E9=87=8D=E9=80=89=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E9=80=80=E9=81=BF=200.2s=20+=20homing=20=E5=88=A4?= =?UTF-8?q?=E5=88=AB=E6=8F=90=E5=88=B0=E8=B0=83=E7=94=A8=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审实测发现两种病态状态下重选会每帧无限重试,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 --- scripts/autoloads/bullet_manager.gd | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/scripts/autoloads/bullet_manager.gd b/scripts/autoloads/bullet_manager.gd index d0cda60..af900e8 100644 --- a/scripts/autoloads/bullet_manager.gd +++ b/scripts/autoloads/bullet_manager.gd @@ -10,6 +10,7 @@ extends Node const BULLET_STRIDE: int = 12 # 禁止裸整数 12(跨切片约束) const MAX_BULLETS: int = 2048 # 预分配 SoA 大小 +const HOMING_RETRY_MS: int = 200 # 归航重选失败后的退避时长(60fps 下把最坏情况摊薄约 12×) # SoA 热数组(由 BulletManagerCs 直接读写) var _data: PackedFloat32Array = PackedFloat32Array() @@ -34,7 +35,9 @@ func _gd_integrate(delta: float) -> void: var i: int = 0 while i < _active_count: 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) # 转向须在位置积分之前,子弹当帧即沿新方向前进 _data[base + 0] += _data[base + 2] * delta # x += vx * 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, 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: var hits: PackedInt32Array = SpatialGrid.query_circle(origin, radius) var best_id: int = -1 @@ -216,7 +219,11 @@ func _find_nearest_unvisited(origin: Vector2, radius: float, visited: Array) -> return best_id ## 归航转向:以 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: var cold: Dictionary = _bullet_contexts[bullet_idx] 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 if EnemyManager.get_active_count() == 0: 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", [])) cold["homing_target_id"] = tid _bullet_contexts[bullet_idx] = cold if tid < 0: + cold["homing_retry_at"] = now + HOMING_RETRY_MS return + cold.erase("homing_retry_at") tpos = EnemyManager.get_pos_by_id(tid) if tpos == Vector2(-9999.0, -9999.0): return