新增 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>
22 KiB
Boss AI 作者指南
文件位置:
Docs/Guides/09_BossAi_Authoring_Guide.md版本:1.0 · 适用项目:zeling_v2 前置阅读:08_EnemyAi_Authoring_Guide(术语、建态原语、常见陷阱在那边讲过,本文不重复)
本文面向给项目新增一个 Boss 的人。核心结论只有一句:
Boss 与小怪跑的是同一条轨。 同一套能力(
EnemyAbilitySO+EnemyAbilityBase)、同一个选招器 (EnemyAttackSelector)、同一套建态原语(AiStateFragments)、同一个决策层组件(EnemyAiBrain)。 Boss 独有的只有三样:手写的AiScript图、一个IBossControl决策面(阶段 + 竞技场锚点)、 以及几个旁挂的 MonoBehaviour(阶段门 / 锚点 / 开战触发)。
不存在"Boss 技能系统"这种独立体系。招式就是能力,阶段就是招池,没有第二套数据模型。
目录
1. Boss 走定制路径
小怪有三条作者路径(配方 / 配方+前置态 / 定制脚本,见 08 号指南 §1)。Boss 一律走第三条:定制脚本。
理由不是"Boss 更高级",而是数量与形状:Boss 只有个位数,彼此差异极大(有的浮空、有的分身、有的换竞技场), 把这些差异塞进可插拔模块的下拉里,只会产生一堆各用一次的模块类。手写图更直接、可读性更好, 且改一个 Boss 不会波及另一个。
写法:
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) // ← 决策层从静置态进战
图里对应的边:
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,
任何条件边都不会再被求值。所以死亡链一条条件边都不能放。
图里只需要两行:
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 态与常量
[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 静置 → 入场
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(地面):逼近 ↔ 选招
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 阶段过渡
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(空中):悬停 ↔ 选招
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 死亡
AiStateFragments.Terminal(b, Death);
b.Global().To(Death).OnEvent(AiSignal.Died);
7.7 三条刻意的形状(改图时别顺手抹掉)
- Boss 不脱战 —— 逼近态的
rest指回自身。 - 阶段单向推进 —— 空中阶段没有回地面阶段的边。
- 起手就打完 —— 攻击态不挂脱战边、不挂阶段边,前摇中玩家跑开仍完整打完。 观感上比"起手到一半凭空收招"合理,也避免了在能力执行途中切阶段导致的状态撕裂。
这三条在源码注释里也写了一遍,因为它们看起来都像"漏挂了一条边"。
8. Boss 专属陷阱
08 号指南 §6 的七条陷阱全部适用,以下是 Boss 图额外的。
-
Build()的 lambda 不能闭包捕获实例。AiGraph按[AiDefinition]id 共享缓存,一张图被所有实例共用。捕获了某个具体 Boss 的组件引用, 第二个实例就会去驱动第一个实例。锚点坐标要经x.Boss.AnchorAt(i)取,不能捕获Transform——BossFragments.MoveToAnchor存在的理由正是这个。 -
PhaseTransition不带出边,必须自己挂。BrainBuilder.Build()只校验"边指向的态是否已声明",不校验"已声明的非终态是否有出边"。 一个没有出边的非终态会让 Boss 永久卡住且零报错。阶段过渡态是这类风险的头号候选。 (补上这项校验是已记录的待办,见 §9。) -
浮空阶段的招不能留
requiresGrounded = true。 Boss 浮空时IsGrounded恒为 false,该字段是选招器的硬门,留默认值会让整个空中招池永远选不中—— 表现为"Boss 悬停着什么都不做",且没有任何报错。 -
阶段过渡的无敌时长必须覆盖演出。
PhaseTxInvincible须 ≥ 浮空上升时长 + 缓冲。配短了 Boss 会在还没升到位时就恢复可受击并进入下一阶段。invincibleDuration <= 0在建图期就抛异常(0 意味着过渡演出期间可被打断,那不是阶段过渡的语义)。 -
BeginPhaseTransition只能在OnEnter调,不能在Tick。 过渡进行中重复调用会被忽略并告警。BossFragments.PhaseTransition已经把它放在OnEnter, 手写阶段态时别照抄成Tick。 -
Boss 的脱战语义与小怪相反。 小怪模块默认挂
LostAllZones脱战边,Boss 一般不挂。别因为"小怪都这么写"就顺手加上。 -
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 里靠肉眼排查 |