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:
@@ -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 记录实测数据与启用结论。
|
||||
Expected:300 弹两种场景 `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 |
|
||||
|
||||
Reference in New Issue
Block a user