docs: 首帧锁定尖峰决策为「接受」+ Task4 Step7 改为两场景单帧峰值回归守卫

spec §5.6:残余的首帧尖峰(1500 弹同帧创建且首次选目标全失败时 16~22ms)
决策接受不处理——触发条件是三个苛刻条件的合取属合成最坏情况、一次性不复发、
压下它需牺牲「首次锁定即时」的手感或改变归航/bounce 语义。附重新立项触发条件。

plan Task4 Step7:原只测「敌人全在射程外」,补上评审发现的更严重场景
「射程内全在 visited」(visited 只增不减故为永久状态);指标由 60 帧均值
改为单帧峰值——退避把成本摊成 1 重选帧 + 11 退避帧,均值会掩盖尖峰,
而帧预算 16.67ms 下尖峰才是玩家可见的。性质由决策关卡降级为回归守卫。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 16:15:01 +08:00
co-authored by Claude Opus 5
parent 5cc40e72e8
commit 4ef8012b6e
2 changed files with 50 additions and 20 deletions
+40 -18
View File
@@ -457,29 +457,51 @@ _mcp_print("FAILS=%d folded=%.2f after_reset=%.2f in_pool=%s" %
Expected: `FAILS=0 folded=6.00 after_reset=0.00 in_pool=true`
> API 已确认存在:`SpellRegistry.get_spell(id)`:36)、`get_all_ids()`:39)、`CastStats` 为 `class_name` 注册的 RefCounted(可 `.new()`)。
- [ ] **Step 7: 性能守卫 —— 确认未触发 R-H1 尖峰**
- [ ] **Step 7: 性能回归守卫 —— R-H1 两种病态场景的单帧峰值**
> spec §5 记录的风险:「敌人在场但均超出 `homing_range`」时每颗归航子弹每帧都会 `query_circle`。此步实测该病态场景,决定是否启用逃生方案(方案 3 失败退避)
> **本步性质已变更(2026-07-30,Task 2 代码质量评审后)**:原本这是「决策关卡」—— 实测后再定要不要启用 spec §5 的退避方案。评审在 Task 2 阶段就直接实测并**证实触发**(300 弹规模即越过 2.0 ms 线),故退避 + 抖动**已随 Task 2 落地**。本步因此降级为**回归守卫**:确认退避与抖动仍生效、峰值未回弹
>
> **两种病态场景都要测**(原计划只写了第一种,漏了更糟的第二种):
> 1. **敌人在场但均超出 `homing_range`** —— 玩家朝空处/刚清空的方向开火。瞬态。
> 2. **射程内敌人全在 `visited_targets` 里** —— bounce+homing 组合弹打空一个小簇后。`visited` 只增不减,故这对那颗子弹是**永久状态**,比第一种更严重。这条是评审发现的,原 spec 风险分析漏掉了。
>
> **指标是单帧最大值,不是 60 帧均值。** 退避把成本摊薄成「1 个重选帧 + 11 个退避帧」,均值会掩盖重选帧的尖峰;而帧预算只有 16.67 ms,尖峰才是会被玩家看见的东西。抖动的作用正是打散这个尖峰。
Run: godot-mcp-pro `execute_game_script`
Run: godot-mcp-pro `execute_game_script`(两种场景各跑一次,改 `SCENARIO` 常量)
```gdscript
var SCENARIO := 1 # 1 = 敌人全在射程外;2 = 射程内全 visited
var N_BULLETS := 300 # 也跑一遍 1500 作为压力上限
BulletManager.reset(); EnemyManager.reset(); SpatialGrid.clear()
# 敌人全部远在 homing_range(400) 之外 → 每帧重选且每帧失败
for k in 50:
var eid = EnemyManager.spawn_enemy(Vector2(3000 + k * 10, 0), 1000.0, 0)
SpatialGrid.insert(eid, Vector2(3000 + k * 10, 0))
for k in 300:
BulletManager.spawn_bullet(Vector2(0, k), Vector2(350, 0), 10.0, 6.0, 1.0, 1.0, 0, -1, 0, 0.0,
{"homing_strength": 3.0, "homing_range": 400.0, "homing_target_id": -1})
var t0 := Time.get_ticks_usec()
for k in 60:
var visited := []
if SCENARIO == 1:
for k in 50: # 全部远在 homing_range(400) 之外
var eid = EnemyManager.spawn_enemy(Vector2(3000 + k * 10, 0), 1000.0, 0)
SpatialGrid.insert(eid, Vector2(3000 + k * 10, 0))
else:
for k in 30: # 在射程内,但全部预置进 visited → 重选必然失败
var eid = EnemyManager.spawn_enemy(Vector2(200 + k, 100 + k), 1000.0, 0)
SpatialGrid.insert(eid, Vector2(200 + k, 100 + k))
visited.append(eid)
for k in N_BULLETS:
var cold := {"homing_strength": 3.0, "homing_range": 400.0, "homing_target_id": -1}
if SCENARIO == 2:
cold["visited_targets"] = visited.duplicate()
BulletManager.spawn_bullet(Vector2(0, k), Vector2(350, 0), 30.0, 6.0, 1.0, 1.0, 0, -1, 0, 0.0, cold)
# 跑满一个退避周期以上,捕捉单帧峰值(含重选帧)
var peak_us := 0
var total_us := 0
for k in 40:
var t0 := Time.get_ticks_usec()
BulletManager._gd_integrate(1.0 / 60.0)
var per_frame := float(Time.get_ticks_usec() - t0) / 60.0 / 1000.0 # ms/帧
_mcp_print("per_frame_ms=%.3f threshold=2.000 escape_needed=%s" %
[per_frame, str(per_frame > 2.0)])
var dt_us := Time.get_ticks_usec() - t0
total_us += dt_us
if dt_us > peak_us: peak_us = dt_us
_mcp_print("scenario=%d bullets=%d peak_ms=%.3f mean_ms=%.3f budget=16.670 over_budget=%s" %
[SCENARIO, N_BULLETS, peak_us / 1000.0, total_us / 40.0 / 1000.0, str(peak_us / 1000.0 > 16.67)])
```
Expected: `escape_needed=false``per_frame_ms` 应远低于 2.0
`escape_needed=true`**按 spec §5 启用方案 3**`_apply_homing` 重选失败时写 `cold["homing_retry_at"] = Time.get_ticks_msec() + 200`,重选前先判 `Time.get_ticks_msec() < int(cold.get("homing_retry_at", 0))` 则直接 return;重选成功时抹掉该键。然后重跑本步确认降到阈值以下,并在 spec §5 记录实测数据与启用结论
Expected300 弹两种场景 `peak_ms` 均明显低于 2.0;1500 弹(压力上限)`over_budget=false`
> 若 `over_budget=true`**停下来上报,不要自行加码** —— 那说明需要的是设计级方案(给 `visited_targets` 设上限、或让归航子弹连续失败 N 次后彻底放弃归航),属设计决策而非实现调优
> 注:编辑器/调试构建的绝对值偏高于 release,看**比例与是否越线**而非绝对数字。基线(普通弹、无冷数据)约 1.6 ms @1500 / 0.32 ms @300,可先跑一次作参照。
- [ ] **Step 8: 设计器字段回读往返(验收⑨后半)**
@@ -537,4 +559,4 @@ EOF
| 8 | 两层修饰器 → `homing_add == 6.0` | Task 4 Step 6 |
| 9 | 商店可购 + 设计器字段往返 | Task 4 Step 6 / Step 8 |
| 10 | `validate_script` 通过 / 无目标不崩 | Task 1 Step 6、Task 2 Step 5、Task 3 Step 2、Task 4 Step 4 |
| — | R-H1 风险实测与逃生方案判定(spec §5 | Task 4 Step 7 |
| — | R-H1 退避+抖动的性能回归守卫(两种病态场景 × 单帧峰值,spec §5 | Task 4 Step 7 |