docs(ai): 敌人 AI 框架(BrainGraph)设计——HFSM 替换 Behavior Designer

C# 声明式为唯一事实来源, 可视化从模型自动生成; 只新建决策层+干净能力接口,
现有感知/移动/能力子系统保留; 受击物理FSM用 IsControllable 门显式让位;
交付面: 运行时调试器/Mermaid导出/MCP自省API; v1 试点 E001+ChaoFengBoss.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 13:01:49 +08:00
co-authored by Claude Opus 4.8
parent c7dd125bc0
commit 0ed7353d24
@@ -0,0 +1,283 @@
# 敌人 AI 框架设计(BrainGraph
- 日期:2026-07-03
- 状态:设计已确认,待 review → 转实现计划
- 工作名:**BrainGraph**(最终名待定)
- 命名空间:`BaseGames.AI`(独立 asmdef,零三方依赖)
- 目标:以一套自研、C# 声明式、可 MCP 全自动生成/调试的**分层状态机(HFSM)**决策框架,替换 Behavior DesignerOpsive)作为敌人/NPC AI 的"决策大脑"。
> 规范约束:本设计与最终代码的命名、注释、`[Tooltip]`/`[Header]` 中**不得出现任何参考游戏名**(项目 CLAUDE.md 第 4 条)。讨论中引用的商业游戏仅作内部设计参照,不落代码。
---
## 1. 背景与动机
### 1.1 现状(探查结论)
项目采用"自研外壳 + Opsive Behavior Designer 决策层"双层设计,但决策层处于**"节点库就绪、行为树资产为空"**的半成品状态:
- Opsive Behavior Designer v2GraphDesigner 内核)作为 UPM 包安装,49 个自定义 `BD_*` 任务节点已写好并编译(`GRAPH_DESIGNER` 宏开启)。
- **但没有任何行为树图资产(graph .asset),也没有任何敌人 prefab/场景挂载 `BehaviorTree` 组件。** 决策层是空的。
- `EnemyBase` 已写好 BT 的 Manual-Tick + 5 档 LOD 节流集成(`EnemyBase.cs:606/629`),但因无图资产而未接通。
- 感知/状态/执行三层已完整实现且解耦良好;49 个 `BD_*` 任务全部是**薄壳**,仅调用 `EnemyBase` 门面方法,不直接碰子系统。
### 1.2 痛点
Behavior Designer 的行为树是**手绘的二进制图资产**:
- MCP / 代码**无法生成**,无法 diff,无法自动化调试。
- 决策逻辑的"唯一事实来源"是二进制图,git review 不可读。
因替换沉没成本几乎为零(无既有图资产),现在是重构决策层的最佳时机。
### 1.3 核心设计取向(已与用户逐条确认)
1. **范式**:分层状态机(HFSM)为骨干;帧级攻击编排下沉到现有能力系统,决策层只负责"选招/编排/让位"。**不采用行为树**——对编排式、确定性、帧级精确的横版动作敌人,FSM 更可控、更好调试。
2. **唯一事实来源**C# 声明式(fluent builder)→ 构建成可自省的不可变 `AiGraph` 模型 →**既执行、又生成可视化**。参数(数值/冷却/距离/权重)外挂 SO 热调。
3. **边界**:只新建"决策大脑 + 干净能力接口(`ISensor`/`IMover`/`ICombatant`/`IActorVitals`)";现有感知/移动/能力子系统保留实现,用薄适配器包接口,彻底甩开 `EnemyBase` 上帝门面。
4. **交付面**:运行时实时调试器 + Mermaid/dot 静态导出 + MCP 运行时自省 API。
---
## 2. 架构总览
```
┌─ 声明层 (C# 唯一事实来源) ───────────────────────────┐
│ EnemyAiScript.Build(b) 用 fluent 描述状态/转换/编排 │ ← MCP 直接写/diff
└───────────────┬─────────────────────────────────────┘
│ 构建一次(每种敌人一份,实例间共享 = 省内存/零结构GC)
┌─ 模型层 (不可变 AiGraph) ────────────────────────────┐
│ States · Transitions · AbilitySequences —— 可自省 │ ← 执行 + 可视化 同源
└───────────────┬───────────────────────┬─────────────┘
执行 │ │ 只读投影
┌─ 运行时 (每实例 AiRuntime) ──────┐ ┌─ 交付面 ───────────────┐
│ 当前状态指针·黑板·计时器·序列游标 │ │ Mermaid导出 / 实时调试器 │
│ 事件驱动转换 + LOD 节流 Tick │ │ / MCP自省API │
│ IsControllable 门(受击让位) │ └─────────────────────────┘
└───────────────┬─────────────────┘
│ 只依赖接口
┌─ 能力接口层 (甩开 EnemyBase 门面) ───────────────────┐
│ ISensor · IMover · ICombatant · IActorVitals │
└───────────────┬─────────────────────────────────────┘
│ 薄适配器
┌─ 执行层 (保留现有实现) ──────────────────────────────┐
│ PhysicsPerceptionSystem · EnemyMovement/NavAgent · │
│ EnemyAbilityRegistry(+子类) · EnemyStats · Poise … │
└──────────────────────────────────────────────────────┘
```
---
## 3. 核心运行时模型
### 3.1 `AiGraph`(不可变模型,每种敌人一份,实例共享)
- 声明一次即构建,包含所有 `State``Transition``AbilitySequence`
- 被运行时执行,也被导出器/调试器读取 —— **同一模型,图与代码绝不失同步**
- flyweight 共享 → 结构零 per-instance 分配、缓存友好;委托在构建期捕获一次。
### 3.2 `AiRuntime`(每敌人实例,极轻)
持有:当前状态路径、`Blackboard`、计时器、正在跑的 `AbilitySequence` 游标。热路径无 LINQ / 装箱。
### 3.3 状态 · 层级 · 转换
- **State** = 可选 `OnEnter/Tick/OnExit` + 出边转换 + 可选子状态机(层级)+ 可选能力编排 `Do(...)`
- **层级(HFSM)**:父状态转换始终高优先评估。"任意态→受击/死亡/被弹反"挂在父层,一处声明、全局生效,替代 BD 的 Selector 优先级堆叠。
- **两类转换,混合驱动**
- **事件转换(push)**:受击、被弹反、发现玩家等信号即时触发,不靠轮询。慢速巡逻杂兵被打的瞬间也立即反应。
- **条件转换(pull**:谓词(`InAttackRange` / `LostFor(2s)`),仅在该状态 LOD 频率下评估。
- 按优先级有序、首个命中即转 → 确定性、好调试。
### 3.4 能力编排(`AbilitySequence`,非帧级微时序)
**关键分工**:帧级编排(一招怎么打,HitBox 时间窗)留在现有 `EnemyAbility` 子类里,本框架**不重造**。决策层的"序列"= 能力编排:
```
选招(按 SO 的 preferredRange/priority/exclusion → Execute()
→ while(IsAbilityRunning) 等待(可被 InterruptAll 打断)
→ AbilityEnded → 重评估 / 接下一招
```
真要新增全新攻击 → 写一个 `EnemyAbility` 子类(属能力系统职责,不归 AI 框架)。
### 3.5 上下文 = 类型化能力接口 + 小黑板(混合)
- **类型化能力接口**(见 §4):状态逻辑调用,无装箱、编译期安全、摆脱 `EnemyBase` 门面。
- **小黑板**(具名可枚举):只放 AI 临时值(目标、最后已知位置、计时器、弹反标志、hp%…)。保留具名黑板是为了让**调试器/MCP 能通用枚举"当前黑板值"**。
### 3.6 受击物理 FSM 的共存(`IsControllable` 显式让位)
`AiPhase`(决策层)与 `EnemyStateType{Controlled,Hurt,Stagger,KnockUp,Dead}`(受击物理 FSM)正交并存。接管规则:
- `IActorVitals.IsControllable = (CurrentState == Controlled)`
- **BrainGraph 只在 `IsControllable` 为真时推进决策**;被打成 Hurt/Stagger/KnockUp 时决策层**挂起**(保留状态指针、不下发新指令;正在等待的能力早被 `TakeDamage``InterruptAll(reason)` 打断)。物理态回 `Controlled` → 原地重评估。
- 弹反/受击/死亡额外作为**父层事件转换**(`OnParried→反应态``OnDied→终止`),高优先即时。
- 好处:调试器能显式显示"决策层:已挂起(KnockUp 中)",不靠隐式 fail 猜测。
### 3.7 Tick 与性能
- **LOD 调度**idle/离屏慢 tick、战斗每帧;中央 `AiScheduler` 时间片分摊避免帧尖峰;**事件转换不受 LOD 影响、永远即时**。
- 共享不可变定义 + 极小实例态 → 低 GC、cache 友好。
---
## 4. 能力接口 ↔ 现有实现 1:1 映射
所有接口方法均有真实实现背书(file:line 见探查报告)。距离比较统一在 `ISensor` 内部用平方距离(对齐 `EnemyStats.SqrDistanceToPlayer`)。
| 接口 | 方法(节选) | 背后真实实现 |
|---|---|---|
| **ISensor** | `SeesPlayer()` / `HasLineOfSight` / `InRange(r)` / `SlotDetects(slot)` / `LostFor(t)` / `LastKnown` | `EnemyBase.IsPlayerVisible()`(ThreatAssessor) · `HasAnyDetection(LOS\|Sight)` · `IsPlayerInRange`(平方) · `IPerceptionSystem.HasAnyDetection(slot)` · 黑板计时 · 黑板(原 `LastKnownPlayerPosition`) |
| **IMover** | `MoveTo(p)` / `MoveDir(d)` / `Stop()` / `Face*/FacePlayer()` / `JumpTo(p)` / `LookAround()` / `WalkRandom()` / `AtDestination` / `IsGrounded/IsNearEdge` / `ReturnHome()` | `IPathAgent.RequestMoveTo` · `EnemyMovement.MoveHorizontal` · `StopMovement` · `FaceTarget/FaceDirection` · `JumpToTarget` · `BeginLookAround` · `WalkToRandom` · `IsAtDestination` · `IsGrounded/IsNearEdge` · `HomePosition` |
| **ICombatant** | `UseAbility(id)` / `ForceUseAbility(id)` / `CanUseAbility(id)` / `IsAbilityRunning(id?)` / `AbilityPhase(id)` / `InterruptAbilities(reason)` / `PickUsable()` / `BasicAttack(type)` / `CanAttack()` | `Registry.Get(id).Execute()/ForceExecute()` · `ability.CanUse/IsRunning/Phase` · `Registry.InterruptAll/InterruptGroup` · 按 `EnemyAbilitySO.preferred*Range/priority/exclusionGroup` 选招 · `BeginAttack(AttackType)` |
| **ICombatant.Boss** | `UseSkill(id)` / `UseSkillWeighted()` / `IsSkillExecuting` / `CurrentPhase` / `BeginPhaseTransition(n)` / `IsPhaseTransitioning` / `SetWeakPoints(on,mult)` | `BossBase.UseBossSkill` · `UseBossSkillWeighted` · `IsBossSkillExecuting` · `CurrentPhase` · `BeginPhaseTransition` · `WeakPointSystem.SetActive` |
| **IActorVitals** | `HpPercent` / `HpBelow(r)` / `IsAlive` / `IsInvincible` / `IsControllable` / `PhysicalState` / `HasStatusEffect(t)` / `ApplyStatusEffect` / `PoiseLevel`/`SetPoiseLevel` | `EnemyStats.CurrentHP/MaxHP` · `IsHPBelow` · `IsAlive/IsInvincible` · `CurrentState==Controlled` · `EnemyStatusEffectManager` · `EnemyPoiseComponent` |
### 4.1 推送信号总线(事件转换来源)
`AiSignal { Damaged, Parried, Staggered, KnockedUp, Died, PlayerSpotted, AbilityEnded, PhaseChanged }`
来源接线(多数已有 C# event):`TakeDamage`(→Damaged) · `ReceiveParry`(→Parried,取代轮询 `ConsumeParryEvent`) · `ForceState`(→Staggered/KnockedUp) · `PerformDeath`(→Died) · `ThreatAssessor` 由假变真(→PlayerSpotted) · `ability.Interrupted`/协程结束(→AbilityEnded) · `BossPhaseEvent`(→PhaseChanged)。
### 4.2 参数外挂(热调落到真实字段)
声明里的 `c.P.*` 解析到已有 SO/字段:`EnemyStatsSO``AttackRange/AttackCooldown/DetectRange/MaxChaseDistance/LoseLinkTimeout/AlertDuration/InvestigateDuration/HomeRadius/HitTiers…`)、各 `EnemyAbility` 子类序列化字段、`EnemyAbilitySO``cooldown/preferredMin/MaxRange/requiresLineOfSight/priority/exclusionGroup`)。
---
## 5. 声明层示例(唯一事实来源)
```csharp
[AiDefinition("E001")]
public sealed class BasicMeleeGruntAi : EnemyAiScript
{
protected override void Build(BrainBuilder b)
{
b.Entry("Patrol");
// 父层:全局反应(事件转换,高优先即时)
// 注意:普通受击/硬直/击飞的"让位"由 IsControllable 门自动处理,无需在此声明。
// 父层事件转换只用于决策层需要的、超出物理动画的额外行为响应。
b.Global()
.To("Dead").OnEvent(AiSignal.Died) // 终止决策
.To("Enrage").When(c => c.Vitals.HpBelow(c.P.EnrageHpRatio)); // 低血狂暴(示例)
b.State("Patrol")
.Tick(c => c.Mover.WalkRandom())
.To("Chase").When(c => c.Sensor.SeesPlayer());
b.State("Chase")
.Tick(c => c.Mover.MoveTo(c.Blackboard.LastKnown))
.To("Combat").When(c => c.Sensor.InRange(c.P.MeleeRange))
.To("Search").When(c => c.Sensor.LostFor(c.P.LoseLinkTimeout));
b.State("Combat")
.OnEnter(c => c.Mover.FacePlayer())
.Do(Seq.Select( // 按 SO 的 preferredRange/priority/exclusion 选招并等待其结束
Ability("slash").When(c => c.Sensor.InRange(c.P.MeleeRange))))
.To("Chase").When(c => !c.Sensor.InRange(c.P.MeleeRange) && c.Combat.NoAbilityRunning);
b.State("Search")
.OnEnter(c => c.Mover.LookAround())
.To("Chase").When(c => c.Sensor.SeesPlayer())
.To("Patrol").After(c => c.P.InvestigateDuration);
}
}
```
- 转换条件的可读标签由 `[CallerArgumentExpression]` 自动从 lambda 源码文本抓取(可选覆写)。
- `c.P.*` = 参数视图,解析自 `EnemyStatsSO` / 能力 SO。
---
## 6. 交付面
### 6.1 Mermaid/dot 导出器
- 纯函数 `AiGraph → stateDiagram-v2` 文本;层级子机 → composite state;边标签取自 `[CallerArgumentExpression]` 抓到的条件文本 / 事件枚举名。
- 入口:菜单 `BaseGames/AI/Export Graph` + 静态方法(打印 Console,供 MCP `unity_execute_code` 抓);文件写 `Docs/AI/<enemyId>.md`
### 6.2 运行时实时调试器(Editor 窗口,仅编辑器)
- 选中敌人 → 实时高亮当前状态、最近 N 次转换(from→to + 触发原因)、黑板表、正在跑能力及 `Phase`/进度、`IsControllable` 门状态。
- 全局 `AiScheduler.Paused` + 单步。
- MVP:状态列表 + 实时高亮 + trace + 黑板;完整自动布局节点图归入后续"设计期只读窗口"。
### 6.3 MCP 运行时自省 API
- 运行时服务 `AiDebugService`(注册进 ServiceLocator),静态入口暴露:
- `ListAgents()` / `GetAgent(id)``{ currentStatePath, isControllable, lastTransitions[], blackboard{}, runningAbility, physicalState }`
- `ExportMermaid(enemyTypeId)``SetPaused(bool)``Step()``ForceState(id, stateName)`(调试驱动)
- MCP 经 `unity_execute_code` 调用即可"问诊"任意敌人 → 生成 + 调试闭环。
---
## 7. 组件、装配与脚手架
### 7.1 组件
- 新增 `EnemyAiBrain : MonoBehaviour`:按 id 从注册表取共享 `AiGraph`、建 `AiRuntime``Awake` 收集兄弟子系统构造四适配器、按 LOD tick。**取代** `EnemyBase``#if GRAPH_DESIGNER` 的 BT 集成。
- `EnemyAiScript`(抽象基类)+ `[AiDefinition("<id>")]` 特性;反射注册表 `AiDefinitionRegistry`
- 适配器 `SensorAdapter/MoverAdapter/CombatantAdapter/VitalsAdapter`:直接引用具体子系统(`Movement/Nav/Registry/Perception/Stats/StatusEffects/Poise`),不经门面。
### 7.2 Domain Reload 约束
项目已关 Domain/Scene Reload。`AiDefinitionRegistry`(静态注册表)与任何静态缓存须用 `RuntimeInitializeOnLoadMethod(BeforeSceneLoad)` 重置;`AiRuntime` 运行时态随 `EnemyAiBrain` 生命周期,无跨播放态残留。
### 7.3 脚手架接线(遵项目 CLAUDE.md 第 2 条)
扩展 `SceneObjectPlacerTool` / `CharacterWizardWindow`:放置敌人时自动挂 `EnemyAiBrain` + 绑定 `AiDefinition` id + 参数 SO。消除 `SceneObjectPlacerTool.cs:354/524``★ 手动挂行为树` TODO,使敌人创建全链路 MCP 可自动化。
---
## 8. 迁移路径(先共存、后退役)
1. 建框架 + 适配器,先在杂兵 `E001_CaoZhi` 跑通(BD 仍在、该敌人不用)。
2. 上齐三交付面(导出 / 调试器 / MCP 自省)。
3. 拿 Boss `ChaoFengBoss` 验证阶段 / 加权技能 / 弱点窗口。
4. 扩展脚手架自动接线。
5. 代表性敌人 parity 后 → 批量迁移剩余 → **最后**删除 49 个 `BD_*``EnemyBase` BT 集成、`com.opsive.*` 包。
> 试点目标(`E001_CaoZhi` / `ChaoFengBoss`)为默认假设,可在 review 时调整。
---
## 9. v1 范围
**纳入 v1**
- 核心运行时(`AiGraph` 模型 + `BrainBuilder` + `AiRuntime` + 层级 + 事件/条件转换 + 能力编排 `AbilitySequence` + `IsControllable` 门 + `AiScheduler` LOD 调度)。
- 4 能力接口 + 适配器。
- `AiSignal` 信号总线接线(对 `EnemyBase` 的最小改动:受击/弹反/死亡/发现玩家/能力结束/阶段变更处 raise 信号)。
- `EnemyAiBrain` 组件 + `AiDefinitionRegistry`
- 三交付面(调试器取 MVP)。
- 脚手架接线。
- 试点:1 杂兵(`E001_CaoZhi`+ 1 Boss`ChaoFengBoss`)。
**推迟到后续**
- 完整节点图 GraphView 设计期只读窗口。
- 数据驱动"新招式免写 C# 子类"(招式继续走 `EnemyAbility` 子类)。
- 批量迁移剩余敌人 + 彻底删 BD/Opsiveparity 后单独收尾)。
- 群体协同精修(先薄封装现有 `AlertNearby`/`ReceiveAlert`)。
---
## 10. 主要程序集与文件(预期)
- 新 asmdef`BaseGames.AI``Assets/_Game/Scripts/AI/`),引用 `BaseGames.Enemies` / `BaseGames.Core`;不引用 `Opsive.*`
- 运行时:`AiGraph.cs``State.cs``Transition.cs``AbilitySequence.cs``AiRuntime.cs``Blackboard.cs``BrainBuilder.cs``EnemyAiScript.cs``AiDefinitionRegistry.cs``AiScheduler.cs``EnemyAiBrain.cs`
- 接口 + 适配器:`ISensor/IMover/ICombatant/IActorVitals` + 4 个 `*Adapter.cs`
- 交付面:`MermaidExporter.cs``AiDebugService.cs``Editor/BrainGraphDebuggerWindow.cs`
- 改动:`EnemyBase.cs`(移除 BT 集成、raise `AiSignal`)、`SceneObjectPlacerTool.cs` / `CharacterWizardWindow.cs`(自动接线)。
---
## 11. 验收标准(v1
1. `E001_CaoZhi` 完全由 BrainGraph 驱动,行为达到"巡逻→发现→追击→近战选招→丢失搜查→归位"闭环,且**不挂任何 `BehaviorTree` 组件**。
2. `ChaoFengBoss` 由 BrainGraph 驱动多阶段 + 加权技能 + 弱点窗口,`BeginPhaseTransition` 无敌演出正确衔接。
3. 受击/弹反:被打时决策层经 `IsControllable` 门正确挂起、能力被打断、恢复后重评估;弹反经父层事件转换进入 Stagger 反应。
4. `BaseGames/AI/Export Graph` 能为上述两个敌人导出与运行行为一致的 Mermaid 图。
5. 运行时经 MCP `unity_execute_code``AiDebugService.GetAgent(id)` 能取到当前状态/转换原因/黑板/能力/门状态。
6. 脚手架放置敌人时自动挂 `EnemyAiBrain` 并绑定定义 + 参数 SO(无手动步骤)。
7. 通过项目自检:SO 校验、AddressKey 校验、Physics2D 层校验无新增错误。