23 KiB
敌人 AI 框架设计(BrainGraph)
- 日期:2026-07-03
- 状态:设计已确认,待 review → 转实现计划
- 工作名:BrainGraph(最终名待定)
- 命名空间:
BaseGames.AI(独立 asmdef,零三方依赖) - 目标:以一套自研、C# 声明式、可 MCP 全自动生成/调试的**分层状态机(HFSM)**决策框架,替换 Behavior Designer(Opsive)作为敌人/NPC AI 的"决策大脑"。
规范约束:本设计与最终代码的命名、注释、
[Tooltip]/[Header]中不得出现任何参考游戏名(项目 CLAUDE.md 第 4 条)。讨论中引用的商业游戏仅作内部设计参照,不落代码。
1. 背景与动机
1.1 现状(探查结论)
项目采用"自研外壳 + Opsive Behavior Designer 决策层"双层设计,但决策层处于**"节点库就绪、行为树资产为空"**的半成品状态:
- Opsive Behavior Designer v2(GraphDesigner 内核)作为 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 核心设计取向(已与用户逐条确认)
- 范式:分层状态机(HFSM)为骨干;帧级攻击编排下沉到现有能力系统,决策层只负责"选招/编排/让位"。不采用行为树——对编排式、确定性、帧级精确的横版动作敌人,FSM 更可控、更好调试。
- 唯一事实来源:C# 声明式(fluent builder)→ 构建成可自省的不可变
AiGraph模型 →既执行、又生成可视化。参数(数值/冷却/距离/权重)外挂 SO 热调。 - 边界:只新建"决策大脑 + 干净能力接口(
ISensor/IMover/ICombatant/IActorVitals)";现有感知/移动/能力子系统保留实现,用薄适配器包接口,彻底甩开EnemyBase上帝门面。 - 交付面:运行时实时调试器 + 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 友好。
3.8 多实例复用与对象池(同一场景多个相同敌人)
- 共享
AiGraph(每敌人类型 1 份,flyweight):状态图结构、转换、能力编排、条件 lambda、显式条件标签字符串,全部只构建一次、由该类型所有实例共享。场景放 N 个相同杂兵 → 结构零 per-instance 分配、无重复 GC、缓存友好。 - 每实例
AiRuntime(N 份):仅当前状态指针、Blackboard、计时器、AbilitySequence游标随实例独立演化。 - 无实例捕获约束:声明里的条件/动作 lambda 只能通过参数
c(上下文)访问实例,不得闭包捕获具体敌人实例——这是 lambda 能挂在共享图上的前提。由BrainBuilder约定保证(后续可加 Roslyn 分析器强校验)。 - 对象池重置:
EnemyBase : IPoolable(池复用走ForceStateRespawn)。敌人回池再生时,EnemyAiBrain必须重置AiRuntime:回到 Entry 状态、清空黑板/计时器/游标、清空 trace。共享AiGraph不重置(无状态)。挂进IPoolable的回收/取出回调。 - 调度分摊:
AiScheduler对同类多实例做 tick 时间片错峰(见 §3.7),避免 N 个相同敌人同帧集中评估造成尖峰。
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. 声明层示例(唯一事实来源)
[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), "HpBelow(enrage)"); // 低血狂暴(示例)
b.State("Patrol")
.Tick(c => c.Mover.WalkRandom())
.To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer");
b.State("Chase")
.Tick(c => c.Mover.MoveTo(c.Blackboard.LastKnown))
.To("Combat").When(c => c.Sensor.InRange(c.P.MeleeRange), "InRange(melee)")
.To("Search").When(c => c.Sensor.LostFor(c.P.LoseLinkTimeout), "LostFor(timeout)");
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), "InRange(melee)")))
.To("Chase").When(c => !c.Sensor.InRange(c.P.MeleeRange) && c.Combat.NoAbilityRunning, "outOfMelee");
b.State("Search")
.OnEnter(c => c.Mover.LookAround())
.To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer")
.To("Patrol").After(c => c.P.InvestigateDuration);
}
}
- 转换条件的可读标签走显式可选字符串(
When(cond, "SeesPlayer")):本项目 Unity 2022.3 = C# 9,不用 C# 10 的[CallerArgumentExpression];不传标签则回退"cond"。将来升级语言级别可无痛切换为自动抓取。 c.P.*= 参数视图,解析自EnemyStatsSO/ 能力 SO。
5b. 敌人感知 ↔ 状态模型(待机 / 警觉 / 追击 / 巡逻)
地面敌人共用的感知驱动状态模型。职责边界铁律:状态转换逻辑在 AI 状态机层;感知系统只回答"在不在某感知区"(纯传感器,不知道有哪些状态)。
5b.1 两种感知(感知门面 EnemyBase)
- 追逐感知
InChaseZone()(内,aggro槽):玩家进入 → 触发追击。 - 视野感知
InVisionZone()(外,los/sight槽;无视野槽则回退追逐感知):合并了"警觉触发"与"追击维持"两职——未发现态进入 → 警觉;追击中脱离 → 退出追击。 - 推荐嵌套 视野 ⊇ 追逐(空间滞后:追逐区触发、视野区维持,防边界抖动)。
5b.2 勾选项
EnemyStatsSO.HasAlertState(bool):是否有警觉状态。false 时未发现态进入追逐区直接追击、不经警觉;视野仅作追击维持。
5b.3 统一逐帧规则(PerceptionStateMachine.Add)
全局:Died → Death
待机/巡逻(R): 在追逐区 → 追击(优先)
否则 HasAlert 且 在视野 → 警觉
否则 保持
警觉: 在追逐区 → 追击
否则 脱离视野 → R
否则 保持警觉(朝向玩家、警觉动画+feedback)
追击: 委托追击能力(如 e001_chase)实现移动/伤害
脱离视野 → R(回 Idle/Patrol,**永不回警觉**);退出时 AI 中断追击能力
核心不对称:警觉只在升级路径(未发现态→警觉→追击)出现;降级(追击→脱离视野)永不经过警觉,直接回 R(该敌人配置的静止态 Idle 或 Patrol)。回 R 后玩家再进视野 → 会再警觉(新一次升级,符合 E001 每次重新发现再张口)。
5b.4 缺省自动降级(一套规则覆盖所有配置)
- 无警觉(
HasAlert=false):警觉分支永不触发。 - 无视野槽:维持退化用追逐感知(脱离追逐区即退出追击)。
- 视野被追逐包含(误配 S⊆C):等价"无视野",建议校验告警。
5b.5 复用与实现
PerceptionStateMachine(BaseGames.Enemies):一次性声明四态+统一规则,各敌人只传Config(HasAlert 由IEnemyActor.HasAlertState运行时读、警觉/追击/巡逻的 OnEnter 钩子、追击能力 id、R)。- E001 = 配置:Idle=伪装、Alert=张口(
e001_alert)、Chase=委托e001_chase、R=巡逻、HasAlert=true。 ContactChaseAbility为纯执行器(追击直到被 AI 中断);退出追击的决策归 AI(OnChaseExit → Combat.InterruptAbilities())。- 动画:AI 经
IEnemyActor.SetPhase(AiPhase)声明阶段,EnemyBase.SetAiPhase播放(Idle/Alert/Chase/Patrol 各自 clip)。
6. 交付面
6.1 Mermaid/dot 导出器
- 纯函数
AiGraph → stateDiagram-v2文本;层级子机 → composite state;边标签取自转换的显式标签字符串 / 事件枚举名。 - 入口:菜单
BaseGames/AI/Export Graph+ 静态方法(打印 Console,供 MCPunity_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. 迁移路径(先共存、后退役)
- 建框架 + 适配器,先在杂兵
E001_CaoZhi跑通(BD 仍在、该敌人不用)。 - 上齐三交付面(导出 / 调试器 / MCP 自省)。
- 拿 Boss
ChaoFengBoss验证阶段 / 加权技能 / 弱点窗口。 - 扩展脚手架自动接线。
- 代表性敌人 parity 后 → 批量迁移剩余 → 最后删除 49 个
BD_*、EnemyBaseBT 集成、com.opsive.*包。
试点目标(
E001_CaoZhi/ChaoFengBoss)为默认假设,可在 review 时调整。
9. v1 范围
纳入 v1:
- 核心运行时(
AiGraph模型 +BrainBuilder+AiRuntime+ 层级 + 事件/条件转换 + 能力编排AbilitySequence+IsControllable门 +AiSchedulerLOD 调度)。 - 4 能力接口 + 适配器。
AiSignal信号总线接线(对EnemyBase的最小改动:受击/弹反/死亡/发现玩家/能力结束/阶段变更处 raise 信号)。EnemyAiBrain组件 +AiDefinitionRegistry。- 三交付面(调试器取 MVP)。
- 脚手架接线。
- 试点:1 杂兵(
E001_CaoZhi)+ 1 Boss(ChaoFengBoss)。
推迟到后续:
- 完整节点图 GraphView 设计期只读窗口。
- 数据驱动"新招式免写 C# 子类"(招式继续走
EnemyAbility子类)。 - 批量迁移剩余敌人 + 彻底删 BD/Opsive(parity 后单独收尾)。
- 群体协同精修(先薄封装现有
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 集成、raiseAiSignal)、SceneObjectPlacerTool.cs/CharacterWizardWindow.cs(自动接线)。
11. 验收标准(v1)
E001_CaoZhi完全由 BrainGraph 驱动,行为达到"巡逻→发现→追击→近战选招→丢失搜查→归位"闭环,且不挂任何BehaviorTree组件。ChaoFengBoss由 BrainGraph 驱动多阶段 + 加权技能 + 弱点窗口,BeginPhaseTransition无敌演出正确衔接。- 受击/弹反:被打时决策层经
IsControllable门正确挂起、能力被打断、恢复后重评估;弹反经父层事件转换进入 Stagger 反应。 BaseGames/AI/Export Graph能为上述两个敌人导出与运行行为一致的 Mermaid 图。- 运行时经 MCP
unity_execute_code调AiDebugService.GetAgent(id)能取到当前状态/转换原因/黑板/能力/门状态。 - 脚手架放置敌人时自动挂
EnemyAiBrain并绑定定义 + 参数 SO(无手动步骤)。 - 多实例复用:同一场景放多个相同敌人时,共享同一
AiGraph(每类型仅构建 1 次),各自AiRuntime独立互不干扰;敌人经对象池回收再生后AiRuntime完全重置、无上一条命的状态残留。 - 通过项目自检:SO 校验、AddressKey 校验、Physics2D 层校验无新增错误。