From 28f4047096029c4dba4ae67355ad0ba4b2531113 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 30 Jul 2026 16:26:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(homing):=20bounce=20=E5=8D=8F=E5=90=8C?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=81=97=E7=95=99=E8=BF=87=E6=9C=9F=E9=80=80?= =?UTF-8?q?=E9=81=BF=E9=94=AE=EF=BC=8C=E4=BD=BF=E5=AD=90=E5=BC=B9=E5=AF=B9?= =?UTF-8?q?=E7=9D=80=E5=B0=B8=E4=BD=93=E8=BD=AC=E5=90=91=E4=B8=94=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=E9=87=8D=E6=96=B0=E9=94=81=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit homing_target_id 有两个写入方,不变式「homing_retry_at 只在目标无效时才有意义」 只在其中一方成立:_apply_homing 重选成功时会 erase 该键,bounce 那行不会。 后果是一颗 bounce+homing 子弹若先经历过一次重选失败、再弹跳到 next_id、而 next_id 随即死亡(弹跳目标是刚打过的敌人,随即死亡是常态),它会对着尸体继续转向,并在 残留退避窗口内(最多 400ms ≈ 24 帧)拒绝重新锁定,哪怕 50px 外就有活敌人。 这恰好废掉了那行 bounce 协同代码存在的意义。 修法与重选成功路径对称,补一行 cold.erase("homing_retry_at")。运行时复现确认: 阶段1 重选失败 -> retry_at 存在=true, tgt=-1 F bounce 后: tgt=3 (expect 3) retry_key_still_present=false (修前 true) F 目标死亡后能否立刻重选: tgt=4 重选成功=true (修前被残留退避压住停在 3) 另两处: - 调用方守卫注释的「省约 87%」改为 64% —— 87% 出自代理微基准推算,已被真实 端到端实测(2.107→1.812ms)推翻,热路径里挂陈旧乐观数字会误导后来人判断该守卫去留。 - jitter 取模的 6 行不可达性论证压缩为一行(运算保留作索引越界兜底,成本为零)。 Co-Authored-By: Claude Opus 5 --- scripts/autoloads/bullet_manager.gd | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/autoloads/bullet_manager.gd b/scripts/autoloads/bullet_manager.gd index f112cc0..d79386f 100644 --- a/scripts/autoloads/bullet_manager.gd +++ b/scripts/autoloads/bullet_manager.gd @@ -39,7 +39,7 @@ func _gd_integrate(delta: float) -> void: while i < _active_count: var base: int = i * BULLET_STRIDE # homing 判别在调用方完成:绝大多数冷数据子弹(纯 pierce/bounce/状态/荷载) - # 不归航,避免为它们付一次完整函数调用(实测 1500 弹省约 87%) + # 不归航,避免为它们付一次完整函数调用(实测 1500 弹省约 64%) 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 @@ -123,6 +123,7 @@ func _check_collision(bullet_idx: int, base: int) -> bool: _data[base + 7] *= float(cold.get("bounce_decay", 0.9)) if cold.has("homing_strength"): cold["homing_target_id"] = next_id # bounce 选目标,homing 接手追上去 + cold.erase("homing_retry_at") # 新目标有效,撤销退避(与重选成功路径对称) cold["bounce_remaining"] = bounce - 1 cold["visited_targets"] = visited _bullet_contexts[bullet_idx] = cold @@ -258,9 +259,9 @@ func _apply_homing(bullet_idx: int, base: int, delta: float) -> void: # 达 22.5ms,直接击穿 16.67ms 帧预算)。 # 用「bullet_idx 在活跃弹数中的占比」而非 bullet_idx % HOMING_RETRY_MS:后者在 # 弹数少于窗口宽度时只铺开 _active_count 毫秒(20 颗弹 → 20ms ≈ 1.2 帧,等于没 - # 打散),占比式则不论弹数多少都铺满整窗。取模仅作越界兜底(循环内 bullet_idx - # < _active_count 已保证不越界),使到期时刻无条件落在 [200, 400) ms。 + # 打散),占比式则不论弹数多少都铺满整窗。到期时刻落在 [200, 400) ms。 var jitter: int = (bullet_idx * HOMING_RETRY_MS) / maxi(1, _active_count) + # 取模在循环内不可达(bullet_idx < _active_count ⟹ jitter ≤ 199),仅作索引越界兜底 cold["homing_retry_at"] = now + HOMING_RETRY_MS + jitter % HOMING_RETRY_MS return cold.erase("homing_retry_at")