Files
zeling_v2/Docs/Guides/09_BossAi_Authoring_Guide.md
T
joywayerandClaude Opus 5 6d3816d7ef 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>
2026-07-30 17:14:32 +08:00

22 KiB
Raw Blame History

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 走定制路径
  2. 可用的建态原语
  3. 阶段 = 换招池
  4. 一招怎么配
  5. 开战怎么触发
  6. 死亡不在 AI 层
  7. 完整范例:嘲风
  8. Boss 专属陷阱
  9. 创建链路与自检

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) 进入 / 每帧声明移动意图,离开时停。modeLocomotionMode.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 < 0invincibleDuration <= 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 上的一个字段,也不是选招器里的一个过滤条件。阶段就是"哪些能力组件当前是启用的"。

承载它的是 BossPhaseAbilityGateAssets/_Game/Scripts/Enemies/Boss/BossPhaseAbilityGate.cs), 挂在 Boss 根节点上,Inspector 里配一张表:

含义
ability 受阶段管控的能力组件(EnemyAbilityBase
phases 该能力可用的阶段索引数组;留空 = 全阶段可用

ApplyPhase(phase) 把该阶段的启用集刷到所有登记的能力上:ability.enabled = 该阶段是否放行

这条链之所以成立,是因为 EnemyAbilityBase.CanUseenabled 纳入了判据—— 被禁用的能力自动从 EnemyAttackSelector 的候选里消失。阶段门不需要碰选招器,选招器也不知道阶段的存在。

调用时机由 BossBase 直接驱动(不订阅事件频道,保证与阶段切换严格同序):

  • BossBase.OnSpawn()ApplyPhase(0)(对象池复用时回到初始招池)
  • BossBase.EnterPhase(phase)ApplyPhase(phase)

不要在 EnemyAbilitySO 上写阶段字段。 SO 描述的是"这一招是什么",不是"什么时候能用"。

未登记在阶段表里的能力不受阶段门影响,保持自身启用状态——入场演出、位移类能力通常就这么放着。 表里某行漏配能力组件会在 OnValidateDebug.LogError 点名行号。


4. 一招怎么配

一招 = 一个 EnemyAbilitySO 资产 + 一个挂在 Boss 上的 EnemyAbilityBase 子类组件(+ 若干 EnemyAttackSO)。 与小怪完全一致,没有 Boss 专用的招式资产类型。

4.1 EnemyAbilitySOABL_ 前缀)

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 EnemyAttackSOEATK_ 前缀)

一段动作的时间轴描述。判定 / 生成 / 音效 / 无敌帧全部挂在动画 clip 的归一化时机上,不写在代码里。

字段 说明
clip Animancer ClipTransition留空则该段瞬间完成(见 §8 与 §9 的已知缺口)
fallbackDuration 无 clip 时的兜底时长
hitBoxSlot / hitBoxEnterT / hitBoxExitT 命中框开关的归一化时机(01
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.WeightedRandomAntiRepeatWeightedRandom,但对上一次选中的招施加权重折扣,压低连续重复。 折扣系数配在 EnemyStatsSO.attackAntiRepeatFactor(0–1)。Boss 招池小、重复感明显,建议开启。


5. 开战怎么触发

Boss 出生停在静置态(LocomotionMode.Idle),不巡逻、不搜敌——等玩家进场。 把它叫醒的是 BossFightTriggerAssets/_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 现有两个值:DiedEngaged。受击 / 硬直不进 AI 图(见 08 号指南 §6 第 5 条)。

漏配处理:BossFightTrigger.Awake 逐条点名报错(触发器未勾 Is Trigger、未指定 Brain、未填 bossId、 两个频道、玩家层掩码为空),任一缺失就整体禁用组件——不做"缺谁跳过谁"的部分执行, 因为半开的战斗(有 BGM 没血条、Boss 不动)比彻底不开更难查。


6. 死亡不在 AI 层

与小怪同一条规则,原因见 08 号指南 §6 第 3 条:EnemyBase.PerformDeathForceState(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%");

一行建两个态:GroundLocomotion.Pursue 逼近)与 GroundAttackCombat.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.BeginPhaseTransitionIsPhaseTransitioning = 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 三条刻意的形状(改图时别顺手抹掉)

  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 WizardBoss 标签页
在场景里放 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 无受击动画时永久卡死 EnterAnimConfig.Hurt 为空时提前返回且不安排回到 Controlled,敌人被打一次就永久停在 HurtIsControllable 恒假、AI 永久停摆。当前美术未接入 = 大部分敌人一挨打就废。优先级最高的已知缺陷
池化敌人复活带着上一条命的冷却 EnemyBase.OnSpawn 的中断调用注释写着"重置能力冷却"但实际没重置
BrainBuilder.Build() 不校验"非终态有无出边" 见 §8 第 2 条
EnemyBase.ReceiveParry 无调用方 伤害管线不带攻击者引用,"谁被弹反谁硬直"暂时接不上。旧的 Boss 侧弹反反制已删除(它的行为是"玩家弹反任意敌人都会让 Boss 硬直",是错的),属于有意的移除且暂无替代
能力资产没有编辑器总览视图 需要一个 EnemyAbilityModule 编辑器窗口,尚未编写
没有 AiDefinitionValidator 能力引用漏配 / 拼错目前只能在 Play Mode 里靠肉眼排查