fix(homing): bounce 协同路径遗留过期退避键,使子弹对着尸体转向且拒绝重新锁定
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 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ func _gd_integrate(delta: float) -> void:
|
|||||||
while i < _active_count:
|
while i < _active_count:
|
||||||
var base: int = i * BULLET_STRIDE
|
var base: int = i * BULLET_STRIDE
|
||||||
# homing 判别在调用方完成:绝大多数冷数据子弹(纯 pierce/bounce/状态/荷载)
|
# homing 判别在调用方完成:绝大多数冷数据子弹(纯 pierce/bounce/状态/荷载)
|
||||||
# 不归航,避免为它们付一次完整函数调用(实测 1500 弹省约 87%)
|
# 不归航,避免为它们付一次完整函数调用(实测 1500 弹省约 64%)
|
||||||
if has_cold and _bullet_contexts.has(i) and _bullet_contexts[i].has("homing_strength"):
|
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
|
||||||
@@ -123,6 +123,7 @@ func _check_collision(bullet_idx: int, base: int) -> bool:
|
|||||||
_data[base + 7] *= float(cold.get("bounce_decay", 0.9))
|
_data[base + 7] *= float(cold.get("bounce_decay", 0.9))
|
||||||
if cold.has("homing_strength"):
|
if cold.has("homing_strength"):
|
||||||
cold["homing_target_id"] = next_id # bounce 选目标,homing 接手追上去
|
cold["homing_target_id"] = next_id # bounce 选目标,homing 接手追上去
|
||||||
|
cold.erase("homing_retry_at") # 新目标有效,撤销退避(与重选成功路径对称)
|
||||||
cold["bounce_remaining"] = bounce - 1
|
cold["bounce_remaining"] = bounce - 1
|
||||||
cold["visited_targets"] = visited
|
cold["visited_targets"] = visited
|
||||||
_bullet_contexts[bullet_idx] = cold
|
_bullet_contexts[bullet_idx] = cold
|
||||||
@@ -258,9 +259,9 @@ func _apply_homing(bullet_idx: int, base: int, delta: float) -> void:
|
|||||||
# 达 22.5ms,直接击穿 16.67ms 帧预算)。
|
# 达 22.5ms,直接击穿 16.67ms 帧预算)。
|
||||||
# 用「bullet_idx 在活跃弹数中的占比」而非 bullet_idx % HOMING_RETRY_MS:后者在
|
# 用「bullet_idx 在活跃弹数中的占比」而非 bullet_idx % HOMING_RETRY_MS:后者在
|
||||||
# 弹数少于窗口宽度时只铺开 _active_count 毫秒(20 颗弹 → 20ms ≈ 1.2 帧,等于没
|
# 弹数少于窗口宽度时只铺开 _active_count 毫秒(20 颗弹 → 20ms ≈ 1.2 帧,等于没
|
||||||
# 打散),占比式则不论弹数多少都铺满整窗。取模仅作越界兜底(循环内 bullet_idx
|
# 打散),占比式则不论弹数多少都铺满整窗。到期时刻落在 [200, 400) ms。
|
||||||
# < _active_count 已保证不越界),使到期时刻无条件落在 [200, 400) ms。
|
|
||||||
var jitter: int = (bullet_idx * HOMING_RETRY_MS) / maxi(1, _active_count)
|
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
|
cold["homing_retry_at"] = now + HOMING_RETRY_MS + jitter % HOMING_RETRY_MS
|
||||||
return
|
return
|
||||||
cold.erase("homing_retry_at")
|
cold.erase("homing_retry_at")
|
||||||
|
|||||||
Reference in New Issue
Block a user