docs(enemy): Boss AI 单轨落地——新增作者指南,清理描述已删除系统的长期文档
新增 Docs/Guides/09_BossAi_Authoring_Guide.md: 定制路径写法([AiDefinition] + AiScript)、建态原语(AiStateFragments + BossFragments + IBossControl)、阶段=换招池、一招怎么配(EnemyAbilitySO + EnemyAttackSO + 动画事件)、开战触发、死亡归物理层, 以已落地的 ChaoFengAi 为逐段范例,附 Boss 专属陷阱与已知缺口。 08 号指南加交叉引用。 删除两份整份描述已删除且从未实现的体系的文档: Docs/Architecture/23_BossSkillModule.md、Docs/Design/47_BossSkillSystem.md。 删前已确认全库无 Markdown 链接残留,索引行与关联文档行改指新指南或 spec。 修订:25_CharacterArchitectureOverview §4 Boss 章节按实际代码重写; 07_EnemyModule 加过时提示、§11 改重定向;02_EventSystem 修正 Boss 事件发送方; 19_BossPatternLibrary 加废止提示(设计意图仍有效,类型名已废); AssetFolderSpec 删旧 Boss 技能编辑器条目;三处 README/索引同步。 Docs_Dev/ 历史记录保留不动。 spec 标记为已实施,并记录实施期偏差(无 Boss 骨架、Telegraph 枚举保留、 阶段直查不走黑板、BossSkillEvent 一并删除)与八项已知未完成项, 其中 EnemyHurtState 无受击动画永久卡死为最高优先级、且与 Boss 轨无关。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,10 @@
|
||||
其中约 95% 只需建一个配方资产、两个下拉选一选,零代码零编译。读完本文档应该能在 5 分钟内判断
|
||||
「我这个敌人该走哪条路径」,并在动手前看到已知的坑。
|
||||
|
||||
> **要写的是 Boss?** Boss 与小怪跑同一条轨(同一套能力、同一个选招器、同一个 `EnemyAiBrain`),
|
||||
> 但一律走定制脚本路径,另有阶段门与竞技场锚点等专属件。
|
||||
> 先读完本文的 §2 / §6,再看 [09_BossAi_Authoring_Guide](09_BossAi_Authoring_Guide.md)。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
@@ -141,7 +145,7 @@ Build(b):
|
||||
|
||||
### E004 蛭母 —— 定制脚本路径(Boss,多技能轮转 + 出场演出)
|
||||
|
||||
出场剧情 → 三技能轮转(撕咬/头槌连段/酸液远程,按距离和 CD/权重选择)→ 死亡分两阶段(挣扎前摇 → 爆体)。技能选择逻辑(按距离分层、CD 轮转)比 `ApproachAttackEngagement` 复杂得多,且需要「玩家绕后触发 Flip、但技能执行中不检测」这类与感知骨架无关的定制转换,属于"完全定制"的判据(与范式无关的独门机制)。Boss 是否要接入 `PerceptionSkeleton` 视具体实现取舍:若拆出"未发现(几乎不存在,出场即战斗)→ 交战(技能轮转视为一个大号 `Attack` 态)"两层意义不大,直接写定制脚本更直接。死亡链遵循 [§6](#6-常见陷阱) 的死亡规则,`Death_Pre`/`Death` 完全交给物理层(`EnemyDeathSequence` 演出 + `EnemyBase.PerformDeath`),AI 图里只需要一个 `Death` 终态兜住全局边。
|
||||
出场剧情 → 三技能轮转(撕咬/头槌连段/酸液远程,按距离和 CD/权重选择)→ 死亡分两阶段(挣扎前摇 → 爆体)。技能选择逻辑(按距离分层、CD 轮转)比 `ApproachAttackEngagement` 复杂得多,且需要「玩家绕后触发 Flip、但技能执行中不检测」这类与感知骨架无关的定制转换,属于"完全定制"的判据(与范式无关的独门机制)。Boss 是否要接入 `PerceptionSkeleton` 视具体实现取舍:若拆出"未发现(几乎不存在,出场即战斗)→ 交战(技能轮转视为一个大号 `Attack` 态)"两层意义不大,直接写定制脚本更直接。死亡链遵循 [§6](#6-常见陷阱) 的死亡规则,`Death_Pre`/`Death` 完全交给物理层(`EnemyDeathSequence` 演出 + `EnemyBase.PerformDeath`),AI 图里只需要一个 `Death` 终态兜住全局边。Boss 图的写法(建态原语、阶段门、开战触发)见 [09_BossAi_Authoring_Guide](09_BossAi_Authoring_Guide.md),已落地的 `ChaoFengAi` 是完整范例。
|
||||
|
||||
### E005 肥蛭 —— 配方路径(精英怪,攻击选择器覆盖远近两招)
|
||||
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
# Boss AI 作者指南
|
||||
|
||||
> 文件位置:`Docs/Guides/09_BossAi_Authoring_Guide.md`
|
||||
> 版本:1.0 · 适用项目:zeling_v2
|
||||
> 前置阅读:[08_EnemyAi_Authoring_Guide](08_EnemyAi_Authoring_Guide.md)(术语、建态原语、常见陷阱在那边讲过,本文不重复)
|
||||
|
||||
本文面向**给项目新增一个 Boss 的人**。核心结论只有一句:
|
||||
|
||||
> **Boss 与小怪跑的是同一条轨。** 同一套能力(`EnemyAbilitySO` + `EnemyAbilityBase`)、同一个选招器
|
||||
> (`EnemyAttackSelector`)、同一套建态原语(`AiStateFragments`)、同一个决策层组件(`EnemyAiBrain`)。
|
||||
> Boss 独有的只有三样:**手写的 `AiScript` 图**、**一个 `IBossControl` 决策面(阶段 + 竞技场锚点)**、
|
||||
> 以及几个旁挂的 MonoBehaviour(阶段门 / 锚点 / 开战触发)。
|
||||
|
||||
不存在"Boss 技能系统"这种独立体系。招式就是能力,阶段就是招池,没有第二套数据模型。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [Boss 走定制路径](#1-boss-走定制路径)
|
||||
2. [可用的建态原语](#2-可用的建态原语)
|
||||
3. [阶段 = 换招池](#3-阶段--换招池)
|
||||
4. [一招怎么配](#4-一招怎么配)
|
||||
5. [开战怎么触发](#5-开战怎么触发)
|
||||
6. [死亡不在 AI 层](#6-死亡不在-ai-层)
|
||||
7. [完整范例:嘲风](#7-完整范例嘲风)
|
||||
8. [Boss 专属陷阱](#8-boss-专属陷阱)
|
||||
9. [创建链路与自检](#9-创建链路与自检)
|
||||
|
||||
---
|
||||
|
||||
## 1. Boss 走定制路径
|
||||
|
||||
小怪有三条作者路径(配方 / 配方+前置态 / 定制脚本,见 08 号指南 §1)。**Boss 一律走第三条:定制脚本。**
|
||||
|
||||
理由不是"Boss 更高级",而是数量与形状:Boss 只有个位数,彼此差异极大(有的浮空、有的分身、有的换竞技场),
|
||||
把这些差异塞进可插拔模块的下拉里,只会产生一堆各用一次的模块类。手写图更直接、可读性更好,
|
||||
且改一个 Boss 不会波及另一个。
|
||||
|
||||
写法:
|
||||
|
||||
```csharp
|
||||
using BaseGames.AI;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
[AiDefinition("<BossId>")] // ← id 字符串,全项目唯一
|
||||
public sealed class XxxAi : AiScript
|
||||
{
|
||||
protected override void Build(BrainBuilder b) { /* 整张图写在这里 */ }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
放在 `Assets/_Game/Scripts/Enemies/AIBrain/Ai/` 下。
|
||||
|
||||
接线:Boss 物体上的 `EnemyAiBrain` 组件,**`_definitionId` 填上面那个 id,`_recipe` 留空**。
|
||||
两者是二选一,`EnemyAiBrain.Awake` 做互斥校验——两个都配或都不配会 `Debug.LogError` 并禁用组件。
|
||||
id 由 `AiDefinitionRegistry` 反射收集所有 `[AiDefinition]` 的 `AiScript` 子类解析,重复 id 在进入 Play 时直接抛异常。
|
||||
|
||||
`AiGraph` 按 id 共享缓存:同一个 `AiScript` 只建一次图,所有实例共用。
|
||||
**直接后果:`Build()` 里的 lambda 绝对不能闭包捕获具体实例**(组件、Transform、Boss 引用一概不行),
|
||||
一切实例数据只能经 `IAiContext x` 参数取。第 8 节还会再提一次,因为这是 Boss 图最容易踩的坑。
|
||||
|
||||
---
|
||||
|
||||
## 2. 可用的建态原语
|
||||
|
||||
建态原语是静态方法,不是可插拔模块——不需要 `[Serializable]`、不进 Inspector 下拉,直接调用。
|
||||
|
||||
### 2.1 通用原语 `AiStateFragments`
|
||||
|
||||
`Assets/_Game/Scripts/Enemies/AIBrain/Modules/AiStateFragments.cs`
|
||||
|
||||
| 方法 | 语义 | 用在哪 |
|
||||
|---|---|---|
|
||||
| `Locomotion(b, state, mode)` | 进入 / 每帧声明移动意图,离开时停。`mode` 取 `LocomotionMode.Idle` / `Face` / `Patrol` / `Pursue`。 | 静置等待、悬停朝向玩家、走位 |
|
||||
| `Ability(b, state, abilityId)` | 每帧 `EnsureAbility`:能力被受击打断后,下一帧自动重新触发。 | "只要还在这个态就该一直在做"的持续态(冲锋、追击) |
|
||||
| `AbilityOnce(b, state, abilityId)` | 只在 `OnEnter` 触发一次,不每帧重触发。 | 一次性演出(入场、变身),**用 `Ability` 会无限重播** |
|
||||
| `Terminal(b, state)` | 无行为终态。 | 死亡态 |
|
||||
| `LostAllZones`(字段) | 共享条件:脱离全部感知区。 | 小怪脱战边;**Boss 一般不用**(见 §8) |
|
||||
|
||||
### 2.2 Boss 原语 `BossFragments`
|
||||
|
||||
`Assets/_Game/Scripts/Enemies/AIBrain/Modules/BossFragments.cs`
|
||||
|
||||
| 方法 | 语义 |
|
||||
|---|---|
|
||||
| `PhaseTransition(b, state, targetPhase, invincibleDuration)` | 阶段过渡态:`OnEnter` 停移动 + 中断能力 + 调 `IBossControl.BeginPhaseTransition`。**刻意不带出边**——调用方必须自己挂 `.To(下一态).When(x => !x.Boss.IsPhaseTransitioning)`,忘了挂 Boss 会永久锁死。`targetPhase < 0` 或 `invincibleDuration <= 0` 在建图期抛异常。 |
|
||||
| `ApproachAttack(b, approach, attack, rest)` | 一组「寻路逼近 ↔ 到射程选招攻击」。转发到 `ApproachAttackEngagement.Declare` 的**同一份实现**,所以小怪的交战模块与 Boss 图行为永远一致。返回**逼近态**的 builder,可继续挂阶段过渡等出边。 |
|
||||
| `MoveToAnchor(b, state, index)` | 移动到第 `index` 个竞技场锚点(`BossArenaAnchors`),到位后由调用方挂出边。 |
|
||||
| `AtAnchor(index, tolerance = 0.2f)` | 共享条件:已到达第 `index` 个锚点。配合上一条用。 |
|
||||
|
||||
### 2.3 Boss 决策面 `IBossControl`
|
||||
|
||||
`Assets/_Game/Scripts/AI/IBossControl.cs`。图里经 `x.Boss` 访问,由 `BossBase` 实现。
|
||||
|
||||
| 成员 | 用途 |
|
||||
|---|---|
|
||||
| `CurrentPhase` | 当前阶段索引(出生为 0) |
|
||||
| `IsPhaseTransitioning` | 是否处于阶段过渡(无敌 + 演出)期间 |
|
||||
| `ResourceFull` | Boss 资源(怒气 / 充能)是否已满。未挂资源组件时**抛异常**——图问了资源却没配组件是配置错误,不静默返回 false |
|
||||
| `BeginPhaseTransition(targetPhase, invincibleDuration)` | 发起阶段过渡。过渡中重复调用会被忽略并告警,所以只能放 `OnEnter`,不能放 `Tick` |
|
||||
| `AnchorAt(index)` / `DistanceToAnchor(index)` | 竞技场锚点坐标 / 距离。漏配组件或下标越界即抛 |
|
||||
|
||||
**非 Boss 敌人访问 `x.Boss` 会抛异常**(`EnemyBrainContext` 里显式失败),这是有意的——
|
||||
在小怪图里误用 Boss 语义应当立刻炸掉,而不是拿到一个假的默认值。
|
||||
|
||||
---
|
||||
|
||||
## 3. 阶段 = 换招池
|
||||
|
||||
阶段不是 SO 上的一个字段,也不是选招器里的一个过滤条件。**阶段就是"哪些能力组件当前是启用的"。**
|
||||
|
||||
承载它的是 `BossPhaseAbilityGate`(`Assets/_Game/Scripts/Enemies/Boss/BossPhaseAbilityGate.cs`),
|
||||
挂在 Boss 根节点上,Inspector 里配一张表:
|
||||
|
||||
| 列 | 含义 |
|
||||
|---|---|
|
||||
| `ability` | 受阶段管控的能力组件(`EnemyAbilityBase`) |
|
||||
| `phases` | 该能力可用的阶段索引数组;**留空 = 全阶段可用** |
|
||||
|
||||
`ApplyPhase(phase)` 把该阶段的启用集刷到所有登记的能力上:`ability.enabled = 该阶段是否放行`。
|
||||
|
||||
这条链之所以成立,是因为 `EnemyAbilityBase.CanUse` 把 `enabled` 纳入了判据——
|
||||
被禁用的能力自动从 `EnemyAttackSelector` 的候选里消失。**阶段门不需要碰选招器,选招器也不知道阶段的存在。**
|
||||
|
||||
调用时机由 `BossBase` 直接驱动(不订阅事件频道,保证与阶段切换严格同序):
|
||||
|
||||
- `BossBase.OnSpawn()` → `ApplyPhase(0)`(对象池复用时回到初始招池)
|
||||
- `BossBase.EnterPhase(phase)` → `ApplyPhase(phase)`
|
||||
|
||||
**不要在 `EnemyAbilitySO` 上写阶段字段。** SO 描述的是"这一招是什么",不是"什么时候能用"。
|
||||
|
||||
未登记在阶段表里的能力不受阶段门影响,保持自身启用状态——入场演出、位移类能力通常就这么放着。
|
||||
表里某行漏配能力组件会在 `OnValidate` 里 `Debug.LogError` 点名行号。
|
||||
|
||||
---
|
||||
|
||||
## 4. 一招怎么配
|
||||
|
||||
一招 = 一个 `EnemyAbilitySO` 资产 + 一个挂在 Boss 上的 `EnemyAbilityBase` 子类组件(+ 若干 `EnemyAttackSO`)。
|
||||
与小怪完全一致,没有 Boss 专用的招式资产类型。
|
||||
|
||||
### 4.1 `EnemyAbilitySO`(`ABL_` 前缀)
|
||||
|
||||
`CreateAssetMenu: BaseGames/Enemies/Enemy Ability`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `abilityId` | 全小写英文 + 下划线,图里 `x.Combat.IsAbilityRunning("...")` 按它匹配 |
|
||||
| `designNote` | 设计备注,只给编辑器看,不影响运行 |
|
||||
| `attackSequence` | `EnemyAttackSO[]`,一招内的多段(如三连挥扇 = 3 个) |
|
||||
| `cooldown` | 冷却(秒),从**执行结束**开始计 |
|
||||
| `interruptOnHurt` / `interruptOnStagger` | 中断规则;`interruptOnHurt = false` 即该招自带霸体 |
|
||||
| `category` | `AbilityCategory.Attack` 的才进选招器候选 |
|
||||
| `weight` / `priority` | 选招权重与优先级 |
|
||||
| `rangeRadius` / `rangeOffset` | **招式自管的圆形触发范围**。`category == Attack` 时必须 `> 0`,否则永远够不着(`Awake` 报错) |
|
||||
| `requiresGrounded` | 选招器的**硬门**。Boss 浮空阶段 `IsGrounded` 恒为 false,空中招留默认 `true` 会永远选不中 |
|
||||
| `requiresLineOfSight` | 同上,视线硬门 |
|
||||
| `exclusionGroup` | 互斥组,同组能力不能同时执行 |
|
||||
|
||||
需要额外可配参数(动画 / 数值 / LayerMask)的招,建 `EnemyAbilitySO` 的**类型化子类**,
|
||||
由能力组件的 `ResolveConfig<T>()` 解析;类型不符显式报错,不做兜底。场景引用(发射点 Transform 等)留在组件上,不进 SO。
|
||||
|
||||
### 4.2 `EnemyAttackSO`(`EATK_` 前缀)
|
||||
|
||||
一段动作的时间轴描述。**判定 / 生成 / 音效 / 无敌帧全部挂在动画 clip 的归一化时机上,不写在代码里。**
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `clip` | Animancer `ClipTransition`。**留空则该段瞬间完成**(见 §8 与 §9 的已知缺口) |
|
||||
| `fallbackDuration` | 无 clip 时的兜底时长 |
|
||||
| `hitBoxSlot` / `hitBoxEnterT` / `hitBoxExitT` | 命中框开关的归一化时机(0–1) |
|
||||
| `damageSource` | `DamageSourceSO` |
|
||||
| `projectileConfig` / `projectileCount` / `spreadAngleDeg` / `projectileFireT` | 弹体招的发射参数与时机 |
|
||||
| `hasPoiseWindow` / `poiseLevel` / `poiseStartT` / `poiseEndT` | 霸体窗口 |
|
||||
| `lockMovement` / `postDelay` | 收招锁移动 / 后摇 |
|
||||
|
||||
### 4.3 动画事件
|
||||
|
||||
`EnemyAnimationEvents.HandleEvent(AnimationEventType, string payload)` 支持:
|
||||
`EnableHitBox` / `DisableHitBox` / `EnableIFrame` / `DisableIFrame` / `SpawnProjectile` /
|
||||
`RoarStart` / `RoarEnd` / `PhaseTwoStart` / `TriggerFeedback` / `PlaySFX` / `AnimationComplete`。
|
||||
|
||||
弹体招的判定跟着 `SpawnProjectile` 生成的弹体走,**Boss 身上不留对应 HitBox**——
|
||||
留一个无人引用的 HitBox 会让下一个作者误以为该招判定挂在 Boss 身上。
|
||||
|
||||
### 4.4 防重复选招
|
||||
|
||||
`EnemyAttackSelector` 新增了 `AttackSelectionMode.WeightedRandomAntiRepeat`:
|
||||
同 `WeightedRandom`,但对上一次选中的招施加权重折扣,压低连续重复。
|
||||
折扣系数配在 `EnemyStatsSO.attackAntiRepeatFactor`(0–1)。Boss 招池小、重复感明显,建议开启。
|
||||
|
||||
---
|
||||
|
||||
## 5. 开战怎么触发
|
||||
|
||||
Boss 出生停在静置态(`LocomotionMode.Idle`),**不巡逻、不搜敌**——等玩家进场。
|
||||
把它叫醒的是 `BossFightTrigger`(`Assets/_Game/Scripts/Enemies/Boss/BossFightTrigger.cs`),
|
||||
放在 Boss 房间入口的触发区上(`Collider2D` + `isTrigger`),只触发一次。
|
||||
|
||||
玩家进入时它做三件事:
|
||||
|
||||
```
|
||||
_onBossFightStarted.Raise(bossId) // EVT_BossFightStarted:全局状态机 / 后处理 / HUD
|
||||
_onBossFightToggled.Raise(true) // EVT_BossFightEnded(布尔开关):血条与 BGM
|
||||
_bossBrain.Send(AiSignal.Engaged) // ← 决策层从静置态进战
|
||||
```
|
||||
|
||||
图里对应的边:
|
||||
|
||||
```csharp
|
||||
AiStateFragments.Locomotion(b, Wait, LocomotionMode.Idle)
|
||||
.To(Intro).OnEvent(AiSignal.Engaged);
|
||||
```
|
||||
|
||||
`AiSignal` 现有两个值:`Died` 与 `Engaged`。受击 / 硬直**不进 AI 图**(见 08 号指南 §6 第 5 条)。
|
||||
|
||||
漏配处理:`BossFightTrigger.Awake` 逐条点名报错(触发器未勾 Is Trigger、未指定 Brain、未填 bossId、
|
||||
两个频道、玩家层掩码为空),任一缺失就整体禁用组件——不做"缺谁跳过谁"的部分执行,
|
||||
因为半开的战斗(有 BGM 没血条、Boss 不动)比彻底不开更难查。
|
||||
|
||||
---
|
||||
|
||||
## 6. 死亡不在 AI 层
|
||||
|
||||
与小怪同一条规则,原因见 08 号指南 §6 第 3 条:`EnemyBase.PerformDeath` 先 `ForceState(Dead)`,
|
||||
之后 `IsControllable` 永久为假,`AiRuntime.Tick` 在让位门处直接 `return`,
|
||||
**任何条件边都不会再被求值**。所以死亡链一条条件边都不能放。
|
||||
|
||||
图里只需要两行:
|
||||
|
||||
```csharp
|
||||
AiStateFragments.Terminal(b, Death);
|
||||
b.Global().To(Death).OnEvent(AiSignal.Died);
|
||||
```
|
||||
|
||||
演出全部归物理层:`EnemyDeathSequence`(前摇演出,`_stopDecisionLayer` 勾上则调 `EnemyBase.NotifyDecisionStop()`)、
|
||||
`EnemyBase.PerformDeath`(关碰撞体、对象池归还)、具体 Boss 的击败序列(如 `ChaoFengBoss` 里的 `NotifyDecisionStop()`)。
|
||||
|
||||
`BossBase` 在死亡时会立即中止阶段过渡(`IsPhaseTransitioning = false`),
|
||||
防止该标志永久锁死影响对象池复用。
|
||||
|
||||
---
|
||||
|
||||
## 7. 完整范例:嘲风
|
||||
|
||||
`Assets/_Game/Scripts/Enemies/AIBrain/Ai/ChaoFengAi.cs` —— 整张图 30 行,逐段拆解。
|
||||
|
||||
### 7.1 态与常量
|
||||
|
||||
```csharp
|
||||
[AiDefinition("ChaoFeng")]
|
||||
public sealed class ChaoFengAi : AiScript
|
||||
{
|
||||
public const string Wait = "Wait", Intro = "Intro";
|
||||
public const string Ground = "Ground", GroundAttack = "GroundAttack";
|
||||
public const string PhaseTx = "PhaseTransition";
|
||||
public const string Air = "Air", AirAttack = "AirAttack";
|
||||
public const string Death = "Death";
|
||||
|
||||
public const string IntroAbility = "chaofeng_intro"; // 与 ABL_ChaoFeng_Intro.abilityId 一致
|
||||
private const float AirPhaseHpRatio = 0.5f; // 50% 血进空中阶段
|
||||
private const int AirPhaseIndex = 1;
|
||||
private const float PhaseTxInvincible = 2f; // 须 ≥ 浮空上升时长 + 缓冲
|
||||
```
|
||||
|
||||
态名用 `public const string` 而不是字面量,是为了让 EditMode 测试(`ChaoFengAiTests`)能断言状态序列
|
||||
而不靠拼字符串。态名本身只是 trace 标签,不驱动动画。
|
||||
|
||||
### 7.2 静置 → 入场
|
||||
|
||||
```csharp
|
||||
b.Entry(Wait);
|
||||
|
||||
AiStateFragments.Locomotion(b, Wait, LocomotionMode.Idle)
|
||||
.To(Intro).OnEvent(AiSignal.Engaged); // ← BossFightTrigger 发的信号
|
||||
|
||||
AiStateFragments.AbilityOnce(b, Intro, IntroAbility)
|
||||
.To(Ground).When(x => !x.Combat.IsAbilityRunning(IntroAbility), "introDone");
|
||||
```
|
||||
|
||||
入场用 `AbilityOnce` 而非 `Ability`:入场是一次性演出,用 `Ability` 会被每帧 `Tick` 无限重播、永远退不出去。
|
||||
|
||||
### 7.3 阶段 0(地面):逼近 ↔ 选招
|
||||
|
||||
```csharp
|
||||
BossFragments.ApproachAttack(b, Ground, GroundAttack, rest: Ground)
|
||||
.To(PhaseTx).When(x => x.Vitals.HpBelow(AirPhaseHpRatio), "hp<50%");
|
||||
```
|
||||
|
||||
一行建两个态:`Ground`(`Locomotion.Pursue` 逼近)与 `GroundAttack`(`Combat.UseBestAttack()` 出招)。
|
||||
`rest: Ground` 指回自身——**Boss 不脱战**,玩家跑出感知区 Boss 也不回静置态。
|
||||
|
||||
`ApproachAttack` 返回的是**逼近态**的 builder,所以后面 `.To(PhaseTx)` 挂在 `Ground` 上:
|
||||
血量掉到 50% 时从逼近态切阶段。攻击态不挂这条边,是为了"起手就打完"(见 7.6)。
|
||||
|
||||
选哪一招不在图里——`Combat.UseBestAttack()` 交给 `EnemyAttackSelector`,
|
||||
射程 / 冷却 / 权重全在各能力自己的 `EnemyAbilitySO` 上。
|
||||
|
||||
### 7.4 阶段过渡
|
||||
|
||||
```csharp
|
||||
BossFragments.PhaseTransition(b, PhaseTx, AirPhaseIndex, PhaseTxInvincible)
|
||||
.To(Air).When(x => !x.Boss.IsPhaseTransitioning, "txDone");
|
||||
```
|
||||
|
||||
`PhaseTransition` 片段**故意不自带出边**,这条 `txDone` 就是它唯一的出口——**删了 Boss 会永久锁死**
|
||||
(`BrainBuilder.Build()` 目前不校验"非终态是否有出边",见 §8)。
|
||||
|
||||
过渡期间的无敌与浮空演出由 Boss 侧承担:`BossBase.BeginPhaseTransition` 置 `IsPhaseTransitioning = true`
|
||||
(`IsInvincible` 随之为真),走完 `invincibleDuration` 后调 `EnterPhase(1)` → `BossPhaseAbilityGate.ApplyPhase(1)`
|
||||
→ 地面四招被禁用、风石招被放行,最后清 `IsPhaseTransitioning`,图上这条边才放行。
|
||||
|
||||
### 7.5 阶段 1(空中):悬停 ↔ 选招
|
||||
|
||||
```csharp
|
||||
AiStateFragments.Locomotion(b, Air, LocomotionMode.Face)
|
||||
.To(AirAttack).When(x => x.Combat.HasEligibleAttack(), "attackInRange");
|
||||
|
||||
b.DeclareState(AirAttack)
|
||||
.OnEnter(x => { x.Locomotion.Stop(); x.Combat.UseBestAttack(); })
|
||||
.OnExit (x => x.Combat.InterruptAbilities())
|
||||
.To(Air).When(x => !x.Combat.IsAbilityRunning(), "attackDone");
|
||||
```
|
||||
|
||||
空中阶段不需要寻路逼近(浮空 + 远程招),所以不用 `ApproachAttack`,改成 `Face`(原地朝向玩家)+ 手写攻击态。
|
||||
攻击态的形状与 `ApproachAttack` 生成的完全一致,只是逼近换成了悬停。
|
||||
|
||||
**刻意没有回地面阶段的边**——阶段单向推进,血量回升也不退阶段(招池已经换了)。
|
||||
|
||||
### 7.6 死亡
|
||||
|
||||
```csharp
|
||||
AiStateFragments.Terminal(b, Death);
|
||||
b.Global().To(Death).OnEvent(AiSignal.Died);
|
||||
```
|
||||
|
||||
### 7.7 三条刻意的形状(改图时别顺手抹掉)
|
||||
|
||||
1. **Boss 不脱战** —— 逼近态的 `rest` 指回自身。
|
||||
2. **阶段单向推进** —— 空中阶段没有回地面阶段的边。
|
||||
3. **起手就打完** —— 攻击态不挂脱战边、不挂阶段边,前摇中玩家跑开仍完整打完。
|
||||
观感上比"起手到一半凭空收招"合理,也避免了在能力执行途中切阶段导致的状态撕裂。
|
||||
|
||||
这三条在源码注释里也写了一遍,因为它们看起来都像"漏挂了一条边"。
|
||||
|
||||
---
|
||||
|
||||
## 8. Boss 专属陷阱
|
||||
|
||||
08 号指南 §6 的七条陷阱**全部适用**,以下是 Boss 图额外的。
|
||||
|
||||
1. **`Build()` 的 lambda 不能闭包捕获实例。**
|
||||
`AiGraph` 按 `[AiDefinition]` id 共享缓存,一张图被所有实例共用。捕获了某个具体 Boss 的组件引用,
|
||||
第二个实例就会去驱动第一个实例。锚点坐标要经 `x.Boss.AnchorAt(i)` 取,不能捕获 `Transform`——
|
||||
`BossFragments.MoveToAnchor` 存在的理由正是这个。
|
||||
|
||||
2. **`PhaseTransition` 不带出边,必须自己挂。**
|
||||
`BrainBuilder.Build()` 只校验"边指向的态是否已声明",**不校验"已声明的非终态是否有出边"**。
|
||||
一个没有出边的非终态会让 Boss 永久卡住且**零报错**。阶段过渡态是这类风险的头号候选。
|
||||
(补上这项校验是已记录的待办,见 §9。)
|
||||
|
||||
3. **浮空阶段的招不能留 `requiresGrounded = true`。**
|
||||
Boss 浮空时 `IsGrounded` 恒为 false,该字段是选招器的硬门,留默认值会让整个空中招池永远选不中——
|
||||
表现为"Boss 悬停着什么都不做",且没有任何报错。
|
||||
|
||||
4. **阶段过渡的无敌时长必须覆盖演出。**
|
||||
`PhaseTxInvincible` 须 ≥ 浮空上升时长 + 缓冲。配短了 Boss 会在还没升到位时就恢复可受击并进入下一阶段。
|
||||
`invincibleDuration <= 0` 在建图期就抛异常(0 意味着过渡演出期间可被打断,那不是阶段过渡的语义)。
|
||||
|
||||
5. **`BeginPhaseTransition` 只能在 `OnEnter` 调,不能在 `Tick`。**
|
||||
过渡进行中重复调用会被忽略并告警。`BossFragments.PhaseTransition` 已经把它放在 `OnEnter`,
|
||||
手写阶段态时别照抄成 `Tick`。
|
||||
|
||||
6. **Boss 的脱战语义与小怪相反。**
|
||||
小怪模块默认挂 `LostAllZones` 脱战边,Boss 一般不挂。别因为"小怪都这么写"就顺手加上。
|
||||
|
||||
7. **`x.Boss` 在非 Boss 上会抛异常。**
|
||||
这是设计如此。把 Boss 原语复制到小怪图里时会立刻炸掉,而不是拿到假数据。
|
||||
|
||||
---
|
||||
|
||||
## 9. 创建链路与自检
|
||||
|
||||
**不要裸建**(CLAUDE.md 第 2 条)。两条脚手架已经按新轨重建:
|
||||
|
||||
| 目的 | 入口 |
|
||||
|---|---|
|
||||
| 建 Boss 的 SO 资产(`EnemyStatsSO` / `EnemyAnimationConfigSO` / 各 `EnemyAbilitySO`) | 菜单 `BaseGames/Data/Character Wizard` → **Boss** 标签页 |
|
||||
| 在场景里放 Boss 完整组件树 | 菜单 `BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)`(`SceneObjectPlacerTool.PlaceChaoFeng`) |
|
||||
| 放开战触发区 | 菜单 `BaseGames/Scene/Place/Boss 开战触发区 (嘲风)`(`SceneObjectPlacerTool.PlaceBossFightTrigger(bossId)`) |
|
||||
| 存成预制体 | 菜单 `BaseGames/Scene/Save Prefab/Boss 嘲风 (ChaoFeng)` |
|
||||
|
||||
放置器产出的组件树(嘲风为例):`ChaoFengBoss`(继承 `BossBase`) + `EnemyStats` + `EnemyFeedback` +
|
||||
`EnemyMovement` + `GroundNavigator` + `EnemyLocomotion` + `EnemyAiBrain` + `BossPhaseAbilityGate` +
|
||||
`PhysicsPerceptionSystem` + 演出组件 + `HurtBox` / `ContactDamageZone` / 各招 `HitBox` / 弹体发射点。
|
||||
|
||||
> `EnemyLocomotion` 是必需的:AI 图的全部移动意图靠它落地。缺了它 `AiStateFragments` 建态时 NRE、
|
||||
> `AiRuntime` 构造失败,Boss 完全不动。
|
||||
|
||||
新增一个 Boss 时,把上述放置器复制一份改名,**不要**在场景里手工 `AddComponent` 拼——
|
||||
脚手架保证了层、命名、事件频道绑定符合规范。
|
||||
|
||||
建完跑三项自检(CLAUDE.md 第 3 条):
|
||||
|
||||
```
|
||||
菜单栏 → BaseGames → Tools → Validation → Validate All ScriptableObjects
|
||||
菜单栏 → BaseGames → Addressables → Validate Address Keys
|
||||
菜单栏 → BaseGames → Tools → Maintenance → Physics2D Layer Matrix → Check
|
||||
```
|
||||
|
||||
### 已知缺口(写 Boss 时会碰到)
|
||||
|
||||
| 缺口 | 影响 |
|
||||
|---|---|
|
||||
| **动画 clip 尚未接入** | `EnemyAttackSO.clip` 留空时该段瞬间完成。目前 Boss 的招"能选中、能跑完、能回到逼近态",但**判定时机、伤害、手感一律未经验证**。clip 到位后必须重新校时序 |
|
||||
| **`EnemyHurtState` 无受击动画时永久卡死** | `Enter` 在 `AnimConfig.Hurt` 为空时提前返回且不安排回到 `Controlled`,敌人被打一次就永久停在 `Hurt`、`IsControllable` 恒假、AI 永久停摆。当前美术未接入 = 大部分敌人一挨打就废。**优先级最高的已知缺陷** |
|
||||
| **池化敌人复活带着上一条命的冷却** | `EnemyBase.OnSpawn` 的中断调用注释写着"重置能力冷却"但实际没重置 |
|
||||
| **`BrainBuilder.Build()` 不校验"非终态有无出边"** | 见 §8 第 2 条 |
|
||||
| **`EnemyBase.ReceiveParry` 无调用方** | 伤害管线不带攻击者引用,"谁被弹反谁硬直"暂时接不上。旧的 Boss 侧弹反反制已删除(它的行为是"玩家弹反任意敌人都会让 Boss 硬直",是错的),属于**有意的移除且暂无替代** |
|
||||
| **能力资产没有编辑器总览视图** | 需要一个 `EnemyAbilityModule` 编辑器窗口,尚未编写 |
|
||||
| **没有 `AiDefinitionValidator`** | 能力引用漏配 / 拼错目前只能在 Play Mode 里靠肉眼排查 |
|
||||
Reference in New Issue
Block a user