调整文档

This commit is contained in:
2026-07-21 10:22:43 +08:00
parent 2d4b3cfd96
commit a7307a5a07
29 changed files with 17 additions and 80 deletions
@@ -0,0 +1,337 @@
# 敌人 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 友好。
### 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. 声明层示例(唯一事实来源)
```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), "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,供 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. **多实例复用**:同一场景放多个相同敌人时,共享同一 `AiGraph`(每类型仅构建 1 次),各自 `AiRuntime` 独立互不干扰;敌人经对象池回收再生后 `AiRuntime` 完全重置、无上一条命的状态残留。
8. 通过项目自检:SO 校验、AddressKey 校验、Physics2D 层校验无新增错误。
@@ -0,0 +1,143 @@
# 敌人执行层统一 + AI/动画/属性瘦身 —— 设计文档
- 日期:2026-07-10
- 分支:`feat/braingraph-enemy-integration`
- 范围:**E001 样板 + 可复用框架**E002-E006/Boss 的 AI 落地范式在本文件定义、后续逐个实现。
- 关联:[[braingraph_ai_framework]]、[[ai_decision_only_delegates_abilities]]、[[enemy_nav_movement_architecture]]spec `2026-07-03-enemy-ai-framework-design.md`
## 1. 背景与问题
敌人 AI 已从三方 Behavior Designer 迁到自研 BrainGraph。四个子系统(movement / stats / state / AI+abilities)通篇审查后暴露三类问题:
1. **两套 AI 并存**~50 个 `BD_*` Behavior Designer 任务 + `BaseGames.Enemies.AI` asmdef + 三个 Opsive 包,运行期全死,只靠 `GRAPH_DESIGNER` 宏还在编译。→ **已在 P1 删除(本文件写作前完成)**
2. **"同一概念多实现"**
- **7 种巡逻**`PatrolAbility`(nav 随机) / `IMover.WalkRandom`(nav 随机, 仅测试) / `BD_WalkRandom` / `BD_InvestigateLastKnown` 子步 / `BD_Patrol`(速度踱步, 撞墙翻向) / `BD_PatrolWaypoints`(路点) / `FlyingDirectNavigator.WalkToRandom`(MovePosition)。既有 4 份"nav 随机游走"重复,又把有玩法差异的"踱步/路点"在迁移中弄丢。
- **4 套移动执行**`EnemyMovement` 直设 velocity / `PendingInput` 信号 / `FlyingEnemy` 直设 velocity / `FlyingDirectNavigator` MovePosition。nav 与 AI 会在同一 FixedUpdate 写同一批 `PendingInput` 字段互相覆盖。
- "朝向"4 入口、"停"4 入口、"设速度"跨 `EnemyStatsSO`/`TBM.movementSpeed`/`MoveInput` 三处。
- `IMover`(含 `UseChaseSpeed/UsePatrolSpeed/LookAround/WalkRandom`)零生产调用,被能力直调 `EnemyBase` 架空。
3. **冗余状态层**`AiPhase`(Idle/Patrol/Alert/Chase/Combat/Investigate/ReturnHome) 与 BrainGraph 状态严格 1:1,只用来选动画、AI 从不回读;`Combat/Investigate/ReturnHome` 是死枚举值。
4. **Model A 过度设计**`IdleAbility`/`PatrolAbility`/`AlertAbility``while(true) yield` 常驻协程,唯一作用是占住 `IsRunning``EnsureAbility` 空转——为琐碎行为各付出「协程+注册项+SO 资产」,且借用了本为攻击设计的 `EnemyAbilitySO``attackSequence/telegraph/range/LOS/priority` 全是死字段)。
5. **属性冗余**`EnemyStatsSO` 约 11/26 字段死或与 sensor 槽 / `DamageSourceSO` 重复;速度配在 3 处;玩家/敌人难度缩放逻辑重复。
**现状要点**:只有 **E001** 有可用 AI`[AiDefinition("E001")]` + `EnemyAiBrain`)。E002/E003/E006/Boss 完全无 AIE004/E005 prefab 里的 "BehaviorTree" 只是 `_stopBehaviorTree` bool 字段,非 Opsive 组件。→ 整套 AI/能力设计只在 1 个敌人上验证过,是重构的最佳时机。
## 2. 设计原则(不变)
- **AI 只决策,不 actuate**AI 决定"进哪个状态 / 转换条件 / 触发哪个能力 / 声明哪个移动意图";移动/朝向/停/速度/动画的**实现**不在 AI。见 [[ai_decision_only_delegates_abilities]]。
- **根因修复,不下游兜底**:删除多余抽象本身(而非留着不调用),让"AI 无法 actuate"在类型层成立。见 CLAUDE.md §6。
- **"能力"一词收回给战斗**:带冷却/预警/HitBox/接触伤害才叫 ability;纯移动/朝向归 locomotion。
## 3. 目标架构
三条正交轴,各自单一职责:
| 轴 | 组件 | 职责 | 谁改它 |
|---|---|---|---|
| 反应态 | `EnemyStateType` FSMControlled/Hurt/Stagger/KnockUp/Dead | 受击/硬直/死亡生命周期;当 AI 的门(`IsControllable`) | 伤害/招架/死亡 |
| 决策 | BrainGraph`AiRuntime`/`PerceptionStateMachine`/`AiScript`) | 感知→状态;声明移动意图 + 触发攻击能力 | 感知查询 |
| 执行 | **`EnemyLocomotion`(新)** + `EnemyAbilityBase`(攻击) | 移动/朝向/步态动画;攻击 | AI 声明意图;能力协程 |
### 3.1 核心组件 `EnemyLocomotion`(统一移动执行器)
单一移动入口,取代 4 套移动模型 + `IMover` + 三个协程能力 + `EnemyBase` 上散落的移动方法。
```csharp
namespace BaseGames.Enemies
{
public enum LocomotionMode { Idle, Patrol, Face, Approach }
public enum PatrolStrategy { Wander, Pace, Waypoints }
// 对外只暴露"声明意图"的 APIAI/能力调用),执行在 FixedUpdate 内部完成
public interface IEnemyLocomotion
{
void SetMode(LocomotionMode mode); // Idle(停)/ Patrol(按配置策略游走)
void Approach(Transform target); // 持续跟随(Chase 能力 / 调查用),派生 RunSpeed
void MoveTo(Vector2 point); // 一次性目标点
void Face(Vector2 lookAt); // 停 + 朝向(Alert 用)
void Stop();
LocomotionMode CurrentMode { get; }
bool IsMoving { get; }
}
}
```
- **内部双后端,作为可替换的 `IPathAgent`**
- **地面怪** = `EnemyNavAgent`(PathBerserker2d)。PB2d 是**表面图寻路**`NavSurface`/`NavSegment` + 表面间 `NavLink`),agent 始终映射到可行走表面——**不支持飞行/自由空间寻路**(已核实包源码:`NavSegmentPositionPointer`/`IsOnLink`/`KeepGrounded`,零 fly/aerial 支持)。
- **飞行怪** = `FlyingDirectNavigator`。**不走 PB2d**,直接 `Rigidbody2D.MovePosition` 向目标点**直线直飞**(含正弦悬停)。
- `EnemyLocomotion` 是二者之上的唯一门面,按敌人类型选后端。物理原生架构(寻路只给方向、velocity 执行、碰撞体底部对齐 y=0)保持,见 [[enemy_nav_movement_architecture]]。
- **巡逻 = `Patrol` 模式 + 每敌人序列化 `PatrolStrategy` + 参数**:一个组件三种玩法(Wander 随机 / Pace 撞墙翻向踱步 / Waypoints 路点序列),消灭 7 份实现并找回踱步/路点。
- **速度单一来源**:由模式派生(Patrol→`WalkSpeed`、Approach→`RunSpeed`,读 `EnemyStatsSO`)。删除 `EnemyMovement``EnemyStatsSO` 的二次直读接线(`EnemyMovement._config`),删除经 `TBM.movementSpeed` 的速度中转重复。
- **`Approach` 不改追击语义**`ContactChaseAbility` 保留为能力;仅把内部 `_enemy.MoveTo(player)` 换成 `Locomotion.Approach(player)`;接触伤害(战斗语义)仍在能力里。
### 3.2 AI 决策层用法(仍"只决策")
`PerceptionStateMachine` 状态不再绑"能力 id",改绑 **locomotion 意图 + 可选攻击能力 id**
```
Idle_Disguise : OnEnter → Locomotion.SetMode(Idle)
Move_Patrol : OnEnter → Locomotion.SetMode(Patrol)
Alert : OnEnter/Tick → Locomotion.Face(player)
Chase : OnEnter → Combat.UseAbility("e001_chase") // 能力内部 Approach(player)
Death : 终态(死亡演出走反应态 FSM)
```
- `SetMode/Face/Approach` 是**声明**(与原 `SetAiPhase` 同性质),执行在 `EnemyLocomotion`——AI 仍不碰 velocity/朝向实现。
- **删除** `IdleAbility`/`PatrolAbility`/`AlertAbility` 三协程能力、`ABL_E001_Idle`/`ABL_E001_Patrol`/`ABL_E001_Alert` 三 SO、`IMover` 接口及其在 `EnemyBrainContext` 的实现。
- `IAiContext` 增加 `IEnemyLocomotion Locomotion { get; }`(替换 `IMover Mover`)。`ICombatant.UseAbility/IsAbilityRunning/InterruptAbilities` 保留(攻击/Chase 用)。
- `PerceptionStateMachine.Config` 改为每状态填 `LocomotionIntent`(模式 + 可选 target 来源)+ 可选 `abilityId``AddAbilityState` 换成 `AddLocomotionState`
### 3.3 动画(删 `AiPhase`
- `EnemyLocomotion` 按当前模式/实际速度驱动步态 clip:静止→`Idle`、Patrol 移动→`Walk`、Approach→`Run`、Face→`Alert`。映射表 = 原 `AiPhase→AnimConfig` switch,改 **key 在 `LocomotionMode`** 上(放 `EnemyLocomotion` 或一个轻量 `EnemyAnimationDriver`)。
- 反应态(Hurt/Stagger/KnockUp/Dead)与攻击/出现能力**照旧各自播 clip**,覆盖在步态之上。
- **删除** `AiPhase` 枚举、`EnemyBase.SetAiPhase`/`CurrentAiPhase`/`OnAiPhaseChanged`,及所有能力里的 `SetAiPhase(...)``ReceiveAlert` 里用 `AiPhase` 做的"已交战不降级"判断改读 AI 当前状态名或一个 `bool IsEngaged`
### 3.4 属性清理(`EnemyStatsSO`
删除死/重复字段:`AttackDamage``AttackRange``DetectRange``DetectAngleDeg``EyeOffset``LOSBlockingMask`(感知全归 sensor 槽 `PhysicsPerceptionSystem`)、`AlertDuration``InvestigateDuration``KnockbackForce``HitStunDuration`(击退归 `DamageSourceSO`)、`HitTierConfig.heavyHitThreshold`(硬直由 Poise `Break` vs `PoiseLevel` 决定)。
- 速度单一来源(删 `EnemyMovement._config` 直读,改经 `EnemyLocomotion``Stats`)。
- (低优先,可选)玩家/敌人难度缩放抽共享 `ScalableVitals` helper(当前两边近乎逐行重复)。
## 4. E002-E006 / Boss 的 AI 落地范式(本文件定义,后续实现)
每个敌人 = **一个 `[AiDefinition("Exxx")]` `AiScript` 子类**,内容仅为:
1. 复用 `PerceptionStateMachine`(或 Boss 专用图),填状态名 + 每状态的 **locomotion 意图** + **攻击能力 id**
2. 攻击行为写成 `EnemyAbilityBase` 子类(已有一批:Melee/Projectile/Leap/Charge/CeilingDrop/…),配 `EnemyAbilitySO`(真攻击才需 cooldown/telegraph/HitBox)。
3. 巡逻/待机/警觉/追击**不写能力**,只在状态里声明 locomotion 意图(含选 `PatrolStrategy`)。
4. 脚手架 `SceneObjectPlacerTool.PlaceExxx``EnemyAiBrain(_definitionId)` + `EnemyLocomotion` + 攻击能力子节点。
近重复能力(后续合并,不阻塞本次):`CeilingDropAbility` vs `AnimatedCeilingDropAbility`(后者注释已声明取代前者);`AppearAbility` = `PlayClipAbility` + 一行 `SetAiPhase``FacePlayerAbility``AlertAbility` 的"朝向"原语;多个攻击能力里手抄的 HitBox 窗口 + `IsGrounded` 射线。
## 5. 分期实施(每期独立可提交、可回滚)
- **P1 死代码清除** — ✅ **已完成**commit `e894a9d`):删 `BD_*` + `BaseGames.Enemies.AI` asmdef + `GRAPH_DESIGNER` 宏 + 三个 `com.opsive.*` 包;恢复编译(0 错误)。
- **P2 引入 `EnemyLocomotion`** — 建组件 + 接入 Nav/Flying 后端;先与旧 API 并存,E001 切过去 PlayMode 跑通四态 + de-escalation。
- **P3 AI 改绑 locomotion 意图** — 删三协程能力/三 SO/`IMover``PerceptionStateMachine``AddLocomotionState``E001CaoZhiAi` 改声明意图;`ContactChaseAbility` 内部改 `Approach`。E001 验证。
- **P4 删 `AiPhase`** — 动画改由 locomotion 驱动;迁移 `SetAiPhase` 全部调用点;`ReceiveAlert` 改判据。
- **P5 Stats 清理** — 删死字段 + 速度单一来源(+ 可选难度缩放 helper)。
- **P6 巡逻策略补全** — 实现 `PatrolStrategy.Pace/Waypoints`(找回丢失玩法)。
- **文档产物** — 本文件即 E002-E06/Boss AI 落地范式;各敌人后续按范式逐个实现(独立任务)。
## 6. 测试
- **EditMode**BrainGraph 现有测试(`Assets/Tests/EditMode/AI/`)保留;`IMover` 相关用例改用 `IEnemyLocomotion` spy 或 `ICombatant` spy 验证状态机 Enter/Tick/Exit 机制。
- **PlayModeTestRoomAE001**:每期验 待机(伪装)→巡逻→警觉(朝向)→追击(接触伤害)→死亡 全链 + 脱离感知 de-escalation;断言 AI 脚本零 `velocity/朝向` 实现、动画随 locomotion 模式切换、console 0 报错。
- **自检**`Validate All ScriptableObjects` / `Validate Address Keys` / `Physics2D Layer Matrix Check`CLAUDE.md §3)。
## 7. 风险与缓解
- **飞行怪双模型**`FlyingEnemy`/`FlyingDirectNavigator``MovePosition`,需在 `EnemyLocomotion` 内以 `IPathAgent` 后端形式收编,避免又留一套并行执行。P2 明确覆盖飞行分支。`EnemyLocomotion` API 对上层一致,`SetMode/Approach/MoveTo` 在飞行后端映射为直飞目标点。
- **飞行绕障是已知缺口(非本次范围)**:PB2d 表面寻路给不了飞行绕障,现有 `FlyingDirectNavigator` 是**直线直飞、无障碍绕行**;当前 E002/E004/E005/E006 prefab 两种导航组件都未挂(飞行导航"有实现未接线")。真要做"能绕墙的飞行怪"时需**单独的飞行寻路方案**(如 2D 网格 A*/点图),不能靠 PB2d 或现有直飞。本次不实现;作为落地飞行怪 AI 时的前置决策记录在案。
- **物理原生易回归**nav 只给方向、velocity 执行、`TBM.SegmentMovement` 关、碰撞体底部 y=0——重构中必须保留,见 [[enemy_nav_movement_architecture]]。
- **动画覆盖顺序**:反应态/攻击 clip 必须能压过步态 clip;P4 需验证受击/攻击时步态不抢播。
- **`AiPhase` 删除波及面**`SetAiPhase` 有多处调用点(能力、`ReceiveAlert`、gizmo/overlay);P4 逐点迁移并保留调试可视化(改读状态名)。
- **范围克制**:本次只在 E001 落地并验证;E002-E006/Boss 仅定范式,避免一次性大改多敌人放大回归面。
## 8. 成功判据
- 编译 0 错误;E001 PlayMode 行为与重构前一致(四态 + de-escalation + 接触伤害)。
- 代码库中"巡逻"实现 1 个(`EnemyLocomotion` + 策略)、"移动执行"入口 1 个、无 `AiPhase`、无 `IMover`、无 Idle/Patrol/Alert 协程能力与其 SO。
- `EnemyStatsSO` 无死字段;速度单一来源。
- E002-E006/Boss 有明确、可照抄的 AI 落地范式文档。
@@ -0,0 +1,116 @@
# 敌人碰撞体:Sprite 驱动 + 统一 Box + 全员接触伤害区 —— 设计文档
- 日期:2026-07-10
- 范围:**只改脚手架 `SceneObjectPlacerTool` 与向导 `CharacterWizardWindow`**;不动现有 prefab(用户后续手动重生成替换)。
## 目标
脚手架生成的每个敌人(含 Boss)满足:
1. **主体碰撞体、HurtBox、ContactDamageZone 三者统一为 `BoxCollider2D`**,尺寸完全一致、**底部对齐 y=0**(敌人原点在碰撞体底部,参考 E001;供 PathBerserker2d NavAgent 检测原点落在导航线上)。
2. 三者尺寸由**向导指定的默认 Sprite 的包围盒**(`sprite.bounds.size`)推导;Sprite 留空则回退到各敌人现有的硬编码尺寸(不破坏无 sprite 的旧行为)。
3. **每个敌人 + Boss 都有 ContactDamageZone**(接触伤害区);不需要接触伤害的敌人由策划把该节点置为非激活。
## 背景(现状)
- `SceneObjectPlacerTool``CreateBodyCollider(go, type, size)` 已支持 Box/Capsule/Circle + `AlignColliderBottomToPivot`(底部对齐)。主体默认 Box。
- HurtBox 各敌人现为 `CapsuleCollider2D`E001 已是 Box);ContactDamageZone 参差:E001=Box、E003/E006=Circle、**E002/E004/E005/ChaoFeng 缺失**。
- 尺寸为每敌人硬编码(如 E001 body 0.6×0.8E002 0.5×0.7…)。
- `EnsureCollidersAreTriggers(go)` 助手已存在(把节点全部 Collider2D 设 trigger)。
- `CharacterWizardWindow` 小怪/ Boss Tab 已有"主体碰撞器类型"EnumField,调 `PlaceSpecificEnemy(id, colliderType)` / `PlaceChaoFeng`
## 设计
### A. 向导:默认 Sprite 字段(可选)
- 小怪 Tab、Boss Tab 各加一个 `UnityEditor.UIElements.ObjectField``objectType = typeof(Sprite)`),标签"默认外观 Sprite(碰撞体尺寸依据,可留空)",绑定 `Sprite _enemyDefaultSprite`Boss 用同一字段或独立 `_bossDefaultSprite`,实现上用一个 `_defaultSprite` 即可,按当前 Tab 复用)。
- 放置按钮把选中 Sprite 传入:`PlaceSpecificEnemy(id, colliderType, _defaultSprite)``PlaceChaoFeng(colliderType, _defaultSprite)`
### B. 脚手架:Sprite 驱动 + 统一 Box + 全员接触伤害区
**新增两个助手:**
```csharp
// Sprite 有则用其世界包围盒尺寸,否则回退。
private static Vector2 SpriteSizeOr(Sprite s, Vector2 fallback)
=> s != null ? (Vector2)s.bounds.size : fallback;
// 建 HurtBox + ContactDamageZone 两个子节点,均为 Box、同尺寸、底部对齐、isTrigger。
// contactEnabledBodyContactDamage 初始启用状态(不需要接触伤害的敌人可置 false 或后续置节点非激活)。
// 返回创建的 (hurtBox, bodyContact, contactHitBox) 供上层按需接线。
private static (HurtBox hurt, BodyContactDamage contact, HitBox contactHitBox)
SetupHurtAndContactBoxes(GameObject root, Vector2 size, bool contactEnabled, List<string> report)
{
// HurtBox 子节点
var hurtT = GetOrCreateChild(root.transform, "HurtBox");
SetLayer(hurtT.gameObject, "EnemyHurtBox", report);
var hurtBox2D = GetOrAddComponent<BoxCollider2D>(hurtT.gameObject);
hurtBox2D.size = size; hurtBox2D.isTrigger = true;
AlignColliderBottomToPivot(hurtBox2D);
var hurtBox = GetOrAddComponent<HurtBox>(hurtT.gameObject);
EnsureCollidersAreTriggers(hurtT.gameObject);
// ContactDamageZone 子节点
var contactT = GetOrCreateChild(root.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
var contactBox = GetOrAddComponent<BoxCollider2D>(contactT.gameObject);
contactBox.size = size; contactBox.isTrigger = true;
AlignColliderBottomToPivot(contactBox);
var contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
var bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
bodyContact.enabled = contactEnabled;
EnsureCollidersAreTriggers(contactT.gameObject);
return (hurtBox, bodyContact, contactHitBox);
}
```
**每个 `PlaceExxx` 签名加可选 sprite;用它推导 size、赋给 SpriteRenderer、走统一助手:**
```csharp
public static void PlaceE0xx(EnemyBodyColliderType bodyCollider = EnemyBodyColliderType.Box,
Sprite defaultSprite = null)
{
...
Vector2 size = SpriteSizeOr(defaultSprite, /*该敌人原硬编码 fallback*/);
var sr = SetupSpriteRenderer(visual.gameObject);
if (defaultSprite != null) sr.sprite = defaultSprite; // 视觉与碰撞体一致
CreateBodyCollider(go, bodyCollider, size); // 主体 Box(size)、底部对齐
var (hurtBox, bodyContact, contactHitBox) =
SetupHurtAndContactBoxes(go, size, contactEnabled: true, report);
// 该敌人特有接线(如 E001contactEnabled=false + 把 bodyContact 绑给 ContactChaseAbility._contactDamage
// contactHitBox 绑 CMB_DS_EnemyBody 伤害源)。
...
}
```
- **主体默认 Box**`EnemyBodyColliderType.Box`);Capsule/Circle 选项保留但默认 Box。三者尺寸都 = `size`
- **接触伤害默认启用**(碰到即伤):`contactEnabled: true`**E001 例外** `contactEnabled: false`(其 ContactChaseAbility 在冲刺期开启接触伤害),并把 `bodyContact` 绑到 `ContactChaseAbility._contactDamage`
- ContactDamageZone 的 HitBox 绑默认伤害源 `CMB_DS_EnemyBody`(沿用 E001 现有做法)。
- Boss `PlaceChaoFeng` 同样通过助手补 ContactDamageZone(默认启用,策划按需置非激活)。
- 全部走 `AlignColliderBottomToPivot`(底部对齐,中心在底部)。
### C. 不动现有 prefab
现有 7 个 prefab 不由本次修改,用户用更新后的脚手架手动重生成替换。
## 影响文件
- `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs`:新增 `SpriteSizeOr` / `SetupHurtAndContactBoxes`;改 7 个 `PlaceExxx`(E001-E006) + `PlaceChaoFeng` 用统一助手 + sprite 参数;替换原 Capsule/Circle 的 HurtBox/ContactDamageZone 创建;给缺失的 4 个敌人补 ContactDamageZone。
- `Assets/_Game/Scripts/Editor/Character/CharacterWizardWindow.cs`:加 `_defaultSprite` 字段 + ObjectField;放置调用传 sprite。
## 验证
- 编译 0 错误。
- 反射/直接调用各 `PlaceExxx` 生成到临时场景对象,校验:
- 主体/HurtBox/ContactDamageZone 都是 `BoxCollider2D`
- HurtBox/ContactDamageZone `isTrigger=true`,主体 `isTrigger=false`
- 三者 `size` 一致(= sprite 包围盒 或 fallback)。
- 三者底部对齐(`offset.y == size.y * 0.5`)。
- 每个敌人 + Boss 均存在 ContactDamageZoneBodyContactDamage 组件在)。
- E001BodyContactDamage 初始 disabled 且已绑到 ContactChaseAbility。
- 用一张测试 Sprite 走一次向导放置,确认碰撞体尺寸 = sprite 包围盒、SpriteRenderer 用了该 sprite。
## 非目标
- 不改现有 prefab(用户手动重生成)。
- 不改敌人 AI / 移动 / 伤害逻辑。
- 不引入 sprite 紧密网格/物理形状(用完整包围盒即可)。