From 023a4a4ec6d26fe522eedecd841796908017e3e0 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 29 Jul 2026 13:54:40 +0800 Subject: [PATCH] =?UTF-8?q?docs(enemy):=20=E5=8F=96=E6=B6=88=E6=AD=BB?= =?UTF-8?q?=E4=BA=A1=E5=B1=82=E2=80=94=E2=80=94=E6=AD=BB=E4=BA=A1=E5=BD=92?= =?UTF-8?q?=E7=89=A9=E7=90=86=E5=B1=82=EF=BC=8C=E5=9B=9B=E5=B1=82=E6=94=B6?= =?UTF-8?q?=E4=B8=BA=E4=B8=89=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T9 质量审查发现 TwoStageDeath 在真实运行中必然卡死:PerformDeath 先 ForceState(Dead) 再 Send(Died),IsControllable 此后永久为假,AiRuntime 的让位门使死亡链里的条件边永不被求值。追根后确认整层放错位置—— 死亡完全归物理层(PerformDeath/EnemyDeathSequence/EnemySpawnerOnEvent), AI 图只需要一个终态让全局 Died 边有去处。 初稿做成一层的由来:旧 Config 的 DeathAbilityId 字段,而其唯一使用者 E001 留空并注明"演出走物理状态机"。投机字段被放大成了三模块的层。 Co-Authored-By: Claude Opus 5 (1M context) --- ...7-29-enemy-ai-composable-modules-design.md | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Docs_Dev/superpowers/specs/2026-07-29-enemy-ai-composable-modules-design.md b/Docs_Dev/superpowers/specs/2026-07-29-enemy-ai-composable-modules-design.md index fda43b14..f7009dc0 100644 --- a/Docs_Dev/superpowers/specs/2026-07-29-enemy-ai-composable-modules-design.md +++ b/Docs_Dev/superpowers/specs/2026-07-29-enemy-ai-composable-modules-design.md @@ -67,7 +67,38 @@ if (c.Engagement == EngagementStyle.ChaseAbility && c.PatrolBetweenChases) ## 3. 架构 -### 3.1 四层 +> ### 修订(2026-07-29,T9 执行中):死亡层取消,四层收为三层 +> +> 初稿把「死亡」做成第三个可插拔层(`IDeathModule` + `TerminalDeath` / `AbilityDeath` / +> `TwoStageDeath`)。实施 T9 时的质量审查发现 `TwoStageDeath` **在真实运行中必然卡死**, +> 追根后确认整个死亡层放错了层。已取消该层,骨架自己声明 `Death` 终态。 +> +> **为什么必然卡死**:`EnemyBase.PerformDeath` 先 `ForceState(EnemyStateType.Dead)` +> (终态,`ForceState` 拒绝任何转出)再 `_brain.Send(AiSignal.Died)`。 +> `EnemyBrainContext.IsControllable` 是 `CurrentState == Controlled`,此时已永久为假。 +> `AiRuntime.Tick` 的顺序是:事件转换(不受门限制)→ **`IsControllable` 让位门,为假即 return** +> → 条件转换。于是 `Died` 事件能把状态切进死亡链,但此后**任何条件边都不再被求值**—— +> `TwoStageDeath` 的 `preDone` 边永远不触发,前段演出播完就永久停住。 +> +> **为什么 `AbilityDeath` 也不成立**:敌人确实能活到下一帧(`PerformDeath` 在死亡动画 +> `OnEnd` 才归还对象池),死亡态的 `OnEnter` 会执行。但那时 `_abilities.InterruptAll(Dead)` +> 已跑过、碰撞体已关、`Dead` 动画已在播——此刻再触发一个能力是在和 `PerformDeath` +> 自己的死亡演出打架。 +> +> **根因**:死亡在本项目里完全归物理层:`PerformDeath`(切终态/清效果/关碰撞/播死亡动画/ +> 归还池/广播事件)、`EnemyDeathSequence`(死亡前摇无敌演出,播完回调 `PerformDeath`; +> 前摇动画上可挂 `SpawnProjectile` 事件配合 `EnemySpawnerOnEvent` 生成小怪——E004/E005 +> 的「两段死亡 + 生成幼蛭」正是由它覆盖)、`EnemyAnimationConfigSO.Dead`。 +> **AI 图需要 `Death` 态只是为了让全局 `Died` 边有去处并让决策停止。死亡不是一个决策。** +> +> 初稿之所以做成一层,是因为旧 `PerceptionStateMachine.Config` 有 `DeathAbilityId` 字段—— +> 而其唯一使用者 E001 把它留空,注释写明「死亡演出走物理状态机」。那个字段从一开始就是 +> 投机的,初稿把投机原样放大成了一个三模块的层。按 YAGNI 与第 6 条取消。 +> +> 受影响:`IDeathModule` 契约删除;三个死亡模块删除;`PerceptionSkeleton.Add` 去掉 +> `death` 参数、自行声明 `Death` 终态;模块总数 8 → 5。 + +### 3.1 三层 ``` PerceptionSkeleton 固定:感知升降级规则(唯一不可插拔的部分)