Files
zeling_v2/Docs_Dev/superpowers/specs/2026-07-03-enemy-ai-framework-design.md
T
2026-07-21 10:22:43 +08:00

23 KiB
Raw Blame History

敌人 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(不可变模型,每种敌人一份,实例共享)

  • 声明一次即构建,包含所有 StateTransitionAbilitySequence
  • 被运行时执行,也被导出器/调试器读取 —— 同一模型,图与代码绝不失同步
  • 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 时决策层挂起(保留状态指针、不下发新指令;正在等待的能力早被 TakeDamageInterruptAll(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、缓存友好。
  • 每实例 AiRuntimeN 份):仅当前状态指针、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/字段:EnemyStatsSOAttackRange/AttackCooldown/DetectRange/MaxChaseDistance/LoseLinkTimeout/AlertDuration/InvestigateDuration/HomeRadius/HitTiers…)、各 EnemyAbility 子类序列化字段、EnemyAbilitySOcooldown/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 复用与实现

  • PerceptionStateMachineBaseGames.Enemies):一次性声明四态+统一规则,各敌人只传 ConfigHasAlert 由 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,供 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、建 AiRuntimeAwake 收集兄弟子系统构造四适配器、按 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 BossChaoFengBoss)。

推迟到后续

  • 完整节点图 GraphView 设计期只读窗口。
  • 数据驱动"新招式免写 C# 子类"(招式继续走 EnemyAbility 子类)。
  • 批量迁移剩余敌人 + 彻底删 BD/Opsiveparity 后单独收尾)。
  • 群体协同精修(先薄封装现有 AlertNearby/ReceiveAlert)。

10. 主要程序集与文件(预期)

  • 新 asmdefBaseGames.AIAssets/_Game/Scripts/AI/),引用 BaseGames.Enemies / BaseGames.Core;不引用 Opsive.*
  • 运行时:AiGraph.csState.csTransition.csAbilitySequence.csAiRuntime.csBlackboard.csBrainBuilder.csEnemyAiScript.csAiDefinitionRegistry.csAiScheduler.csEnemyAiBrain.cs
  • 接口 + 适配器:ISensor/IMover/ICombatant/IActorVitals + 4 个 *Adapter.cs
  • 交付面:MermaidExporter.csAiDebugService.csEditor/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_codeAiDebugService.GetAgent(id) 能取到当前状态/转换原因/黑板/能力/门状态。
  6. 脚手架放置敌人时自动挂 EnemyAiBrain 并绑定定义 + 参数 SO(无手动步骤)。
  7. 多实例复用:同一场景放多个相同敌人时,共享同一 AiGraph(每类型仅构建 1 次),各自 AiRuntime 独立互不干扰;敌人经对象池回收再生后 AiRuntime 完全重置、无上一条命的状态残留。
  8. 通过项目自检:SO 校验、AddressKey 校验、Physics2D 层校验无新增错误。