diff --git a/scripts/autoloads/bullet_manager.gd b/scripts/autoloads/bullet_manager.gd index a22ffb8..f112cc0 100644 --- a/scripts/autoloads/bullet_manager.gd +++ b/scripts/autoloads/bullet_manager.gd @@ -10,7 +10,10 @@ extends Node const BULLET_STRIDE: int = 12 # 禁止裸整数 12(跨切片约束) const MAX_BULLETS: int = 2048 # 预分配 SoA 大小 -const HOMING_RETRY_MS: int = 200 # 归航重选失败后的退避时长(60fps 下把最坏情况摊薄约 12×) +# 归航重选失败后的退避时长(60fps 下把最坏情况摊薄约 12×)。 +# 一值两用且刻意耦合:既是基础退避时长,也是抖动窗口宽度 —— 到期时刻均匀铺满 +# 「恰好一个退避周期」正是消除集体重试尖峰所需的性质,拆成两个常量反而会掩盖这层关系。 +const HOMING_RETRY_MS: int = 200 # SoA 热数组(由 BulletManagerCs 直接读写) var _data: PackedFloat32Array = PackedFloat32Array() @@ -249,7 +252,16 @@ func _apply_homing(bullet_idx: int, base: int, delta: float) -> void: cold["homing_target_id"] = tid _bullet_contexts[bullet_idx] = cold if tid < 0: - cold["homing_retry_at"] = now + HOMING_RETRY_MS + # 抖动「到期时刻」而非让所有子弹共用固定 HOMING_RETRY_MS:同帧集体失败的子弹 + # 拿到同一个 now,退避期完全一致,12 帧后又整齐地一起重试 —— 退避会同步而非 + # 打散,同波齐射天然同相故尖峰永不消散(实测 1500 弹 visited 场景 40 帧里 4 帧 + # 达 22.5ms,直接击穿 16.67ms 帧预算)。 + # 用「bullet_idx 在活跃弹数中的占比」而非 bullet_idx % HOMING_RETRY_MS:后者在 + # 弹数少于窗口宽度时只铺开 _active_count 毫秒(20 颗弹 → 20ms ≈ 1.2 帧,等于没 + # 打散),占比式则不论弹数多少都铺满整窗。取模仅作越界兜底(循环内 bullet_idx + # < _active_count 已保证不越界),使到期时刻无条件落在 [200, 400) ms。 + var jitter: int = (bullet_idx * HOMING_RETRY_MS) / maxi(1, _active_count) + cold["homing_retry_at"] = now + HOMING_RETRY_MS + jitter % HOMING_RETRY_MS return cold.erase("homing_retry_at") tpos = EnemyManager.get_pos_by_id(tid)