docs(ai): BrainGraph 接通敌人阶段(E001+脚手架一键)实现计划

AiDefinitionRegistry(反射按id取共享图)+EnemyBrainContext(单类适配)+EnemyAiBrain组件
+E001 AiScript+EnemyBase去BT接signal+SceneObjectPlacerTool一键挂载+PlayMode集成验证.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 14:52:22 +08:00
co-authored by Claude Opus 4.8
parent 11e4c9f1ea
commit 80864eec6c
@@ -0,0 +1,634 @@
# BrainGraph 接通敌人阶段(E001 + 脚手架一键)实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让脚手架一键放置的 E001(草蛭)敌人,其 AI 完全由自研 BrainGraph 驱动、且**不挂任何 `BehaviorTree` 组件**:新增按 id 解析共享图的注册表、把 EnemyBase 子系统包成能力接口的上下文、承载 `AiRuntime``EnemyAiBrain` 组件、E001 的 AiScript 定义,移除 EnemyBase 的 Opsive BT 集成,并在 `SceneObjectPlacerTool` 里自动挂载/绑定。
**Architecture:** `EnemyAiBrain : MonoBehaviour``_definitionId`(string)`Awake``AiDefinitionRegistry.GetGraph(id)` 取每类型共享的不可变 `AiGraph`,构造 `EnemyBrainContext`(实现 `IAiContext`+四能力接口,委托 EnemyBase 子系统)与 `AiRuntime``Update``Tick`。EnemyBase 只在死亡处 `Send(AiSignal.Died)``OnSpawn``ResetBrain()`,受击让位由 `IsControllable` 门自动处理。脚手架把 `EnemyAiBrain` 挂上并 `AssignString(_definitionId,"E001")`
**Tech Stack:** Unity 2022.3C# 9, Unity Test Framework (EditMode), asmdef, Addressables, MCP(编译/测试/PlayMode 验证)。
**依赖:** BrainGraph 第 1 阶段核心运行时已合并 master(`BaseGames.AI`AiGraph/BrainBuilder/AiRuntime/AiScript/[AiDefinition]/IAiContext/4 接口/Blackboard)。
**Spec** `Docs/superpowers/specs/2026-07-03-enemy-ai-framework-design.md`(本阶段覆盖 §4 适配、§7 组件与脚手架、§8 迁移第 1-2 步的 E001 部分;LOD/AiScheduler、能力编排 sugar、Boss、参数 SO 热调、批量删 BD/Opsive 仍属后续)。
---
## 关键前提(来自代码探查,务必按真实结构对接)
- `SceneObjectPlacerTool.PlaceE001_CaoZhi(EnemyBodyColliderType)``Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs:439`)用 `GetOrAddComponent<T>``AssignReference`/`AssignAsset`/`AssignString`(该文件内 private static,见 :2204/:2244/:2257/:2333)装配敌人;末尾 `report.Add("★ 挂载行为树…E001_CaoZhi.asset。")`(**:524**)是要替换的接线点。E001 已带 `HurtBox` + `ContactDamageZone`(`BodyContactDamage`)——**接触即伤害**,无需攻击能力。
- `EnemyBase``Assets/_Game/Scripts/Enemies/EnemyBase.cs`)暴露:`IsPlayerVisible()``IsPlayerInRange(float)``PlayerTransform``Nav`(`IPathAgent`,含 `IsMoving`/`WalkToRandom`)、`MoveTo(Vector2)``FacePlayer()``StopMovement()``BeginLookAround()``Abilities`(`EnemyAbilityRegistry`)、`IsAlive``CurrentState`(`EnemyStateType`)、`Stats`(`EnemyStats`, `CurrentHP`)、`StatsSO`(`MaxHP`)。`EnemyStateType{Controlled,Hurt,Stagger,KnockUp,Dead}``IPoolable` 只有 `OnSpawn`/`OnDespawn`
- `EnemyBase` 的 Opsive BT 集成全部在 `#if GRAPH_DESIGNER` 内(精确行号见 Task 5)。
- 已关 Domain Reload:静态注册表须 `RuntimeInitializeOnLoadMethod` 重置。
## 文件结构(本阶段)
新增(`BaseGames.AI``Assets/_Game/Scripts/AI/`):
- `AiDefinitionRegistry.cs` — 反射按 id 提供共享 AiGraph。
新增(`BaseGames.Enemies``Assets/_Game/Scripts/Enemies/AIBrain/`):
- `EnemyBrainContext.cs` — 实现 IAiContext+4 接口,委托 EnemyBase 子系统。
- `EnemyAiBrain.cs` — MonoBehaviour,承载 AiRuntime。
- `Ai/E001CaoZhiAi.cs``[AiDefinition("E001")]` 的 AiScript。
修改:
- `Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef` — references 增加 `BaseGames.AI`
- `Assets/_Game/Scripts/Enemies/EnemyBase.cs` — 移除 BT 集成,加 brain 引用/Send(Died)/OnSpawn 重置。
- `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs``PlaceE001_CaoZhi``EnemyAiBrain` + 绑 id。
测试(`Assets/Tests/EditMode/AI/`):
- `AiDefinitionRegistryTests.cs`
> 命名/注释禁参考游戏名(CLAUDE.md §4);根因暴露、禁下游兜底(§6);创建走脚手架(§2)。
---
## Task 1: `AiDefinitionRegistry`(反射按 id 取共享图)
**Files:**
- Create: `Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs`
- Create: `Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs`
- [ ] **Step 1: 写失败测试 `AiDefinitionRegistryTests.cs`**
```csharp
using NUnit.Framework;
using BaseGames.AI;
namespace BaseGames.Tests.EditMode.AI
{
// 顶层测试定义,供反射注册表发现
[AiDefinition("__test_grunt")]
internal sealed class TestGruntAi : AiScript
{
protected override void Build(BrainBuilder b)
{
b.Entry("A");
b.State("A").To("B").When(c => c.Sensor.SeesPlayer(), "SeesPlayer");
b.State("B");
}
}
public class AiDefinitionRegistryTests
{
[Test]
public void Has_FindsRegisteredDefinition()
{
Assert.IsTrue(AiDefinitionRegistry.Has("__test_grunt"));
}
[Test]
public void GetGraph_ResolvesById()
{
var g = AiDefinitionRegistry.GetGraph("__test_grunt");
Assert.AreEqual("A", g.EntryState);
}
[Test]
public void GetGraph_SameId_ReturnsSharedInstance()
{
Assert.AreSame(
AiDefinitionRegistry.GetGraph("__test_grunt"),
AiDefinitionRegistry.GetGraph("__test_grunt"));
}
[Test]
public void GetGraph_UnknownId_Throws()
{
Assert.Throws<System.InvalidOperationException>(
() => AiDefinitionRegistry.GetGraph("__nope__"));
}
}
}
```
- [ ] **Step 2: 运行确认失败**
Run: EditMode `AiDefinitionRegistryTests`(经 MCP`unity_advanced_tool``unity_testing_run_tests` testMode=EditMode)。
Expected: 编译失败 —— `AiDefinitionRegistry` 未定义。
- [ ] **Step 3: 实现 `AiDefinitionRegistry.cs`**
```csharp
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace BaseGames.AI
{
/// <summary>
/// 反射收集所有 [AiDefinition] 的 AiScript 子类,按 id 提供共享 AiGraphflyweight)。
/// 同一 id 全实例共享同一 AiScript 实例 → 同一张不可变 AiGraph。
/// </summary>
public static class AiDefinitionRegistry
{
static Dictionary<string, AiScript> _byId;
// 项目已关闭 Domain Reload:静态缓存需在进入播放时重置,保证按最新类型重建。
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void ResetOnPlay() => _byId = null;
static void EnsureBuilt()
{
if (_byId != null) return;
_byId = new Dictionary<string, AiScript>();
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
Type[] types;
try { types = asm.GetTypes(); }
catch (ReflectionTypeLoadException e) { types = e.Types; }
if (types == null) continue;
for (int i = 0; i < types.Length; i++)
{
var t = types[i];
if (t == null || t.IsAbstract || !typeof(AiScript).IsAssignableFrom(t)) continue;
var attr = t.GetCustomAttribute<AiDefinitionAttribute>();
if (attr == null) continue;
if (_byId.ContainsKey(attr.Id))
throw new InvalidOperationException(
$"AiDefinitionRegistry: 重复的 AI 定义 id '{attr.Id}'{t.FullName})。");
_byId[attr.Id] = (AiScript)Activator.CreateInstance(t);
}
}
}
public static bool Has(string id)
{
EnsureBuilt();
return _byId.ContainsKey(id);
}
/// <summary>取 id 对应的共享 AiGraph;不存在则显式抛错(根因暴露,不做兜底)。</summary>
public static AiGraph GetGraph(string id)
{
EnsureBuilt();
if (!_byId.TryGetValue(id, out var script))
throw new InvalidOperationException(
$"AiDefinitionRegistry: 未找到 AI 定义 id '{id}'。请确认存在 [AiDefinition(\"{id}\")] 的 AiScript 子类。");
return script.GetOrBuildGraph();
}
public static IEnumerable<string> Ids
{
get { EnsureBuilt(); return _byId.Keys; }
}
}
}
```
- [ ] **Step 4: 运行确认通过**
Run: EditMode `AiDefinitionRegistryTests`
Expected: PASS4 项)。全量测试无失败。
- [ ] **Step 5: 提交**
```bash
git add Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs.meta \
Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs.meta
git commit -m "feat(ai): AiDefinitionRegistry——反射按id提供共享AiGraph(关域重载重置)"
```
---
## Task 2: `BaseGames.Enemies` 引用 `BaseGames.AI` + `EnemyBrainContext`
**Files:**
- Modify: `Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef`
- Create: `Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs`
无独立单测(依赖 EnemyBase MonoBehaviour,行为由 Task 7 集成验证);本任务编译校验。
- [ ] **Step 1: 给 `BaseGames.Enemies.asmdef` 的 references 增加 `"BaseGames.AI"`**
先 Read `Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef`,在 `references` 数组里加入一行 `"BaseGames.AI"`(放在 `"BaseGames.Core"` 之后即可)。其余字段不动。
- [ ] **Step 2: 创建 `Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs`**
```csharp
using UnityEngine;
using BaseGames.AI;
namespace BaseGames.Enemies
{
/// <summary>
/// 把 EnemyBase 的子系统包装成 BrainGraph 的能力接口(每敌人一实例)。
/// 状态逻辑只依赖这些接口,不直接引用具体子系统。
/// </summary>
public sealed class EnemyBrainContext : IAiContext, ISensor, IMover, ICombatant, IActorVitals
{
readonly EnemyBase _enemy;
readonly Blackboard _blackboard = new Blackboard();
Vector2 _lastKnown;
float _lostTimer;
public EnemyBrainContext(EnemyBase enemy) { _enemy = enemy; }
// ---- IAiContext ----
public ISensor Sensor => this;
public IMover Mover => this;
public ICombatant Combat => this;
public IActorVitals Vitals => this;
public Blackboard Blackboard => _blackboard;
/// <summary>每帧由 EnemyAiBrain 在 Tick 之前调用,维护最后已知位置与丢失计时。</summary>
public void Refresh(float dt)
{
if (_enemy.IsPlayerVisible() && _enemy.PlayerTransform != null)
{
_lastKnown = _enemy.PlayerTransform.position;
_lostTimer = 0f;
}
else
{
_lostTimer += dt;
}
}
/// <summary>对象池复用时清空临时态。</summary>
public void ResetScratch()
{
_blackboard.Clear();
_lastKnown = _enemy.transform.position;
_lostTimer = 0f;
}
// ---- ISensor ----
public bool SeesPlayer() => _enemy.IsPlayerVisible();
public bool InRange(float range) => _enemy.IsPlayerInRange(range);
public bool LostFor(float seconds) => !_enemy.IsPlayerVisible() && _lostTimer >= seconds;
public Vector2 LastKnown => _lastKnown;
// ---- IMover ----
public void MoveTo(Vector2 target) => _enemy.MoveTo(target);
public void FacePlayer() => _enemy.FacePlayer();
public void Stop() => _enemy.StopMovement();
public void WalkRandom()
{
var nav = _enemy.Nav;
if (nav != null && !nav.IsMoving) nav.WalkToRandom();
}
public void LookAround() => _enemy.BeginLookAround();
// ---- ICombatant ----
public bool UseAbility(string abilityId)
{
var a = _enemy.Abilities?.Get(abilityId);
return a != null && a.Execute();
}
public bool IsAbilityRunning(string abilityId = null)
{
var reg = _enemy.Abilities;
if (reg == null) return false;
if (abilityId != null)
{
var a = reg.Get(abilityId);
return a != null && a.IsRunning;
}
var all = reg.All;
for (int i = 0; i < all.Count; i++)
if (all[i].IsRunning) return true;
return false;
}
public bool NoAbilityRunning => !IsAbilityRunning();
public bool CanUseAbility(string abilityId)
{
var a = _enemy.Abilities?.Get(abilityId);
return a != null && a.CanUse;
}
// ---- IActorVitals ----
public bool IsAlive => _enemy.IsAlive;
public bool IsControllable => _enemy.CurrentState == EnemyStateType.Controlled;
public float HpPercent
{
get
{
var so = _enemy.StatsSO;
return so != null && so.MaxHP > 0 ? (float)_enemy.Stats.CurrentHP / so.MaxHP : 0f;
}
}
public bool HpBelow(float ratio) => HpPercent < ratio;
}
}
```
> 若某个成员的 EnemyBase 属性/方法名与上面不符(如 `Nav` 无 `IsMoving`),**先 Read `EnemyBase.cs`/`IPathAgent.cs`/`EnemyAbilityRegistry.cs` 核对真实签名再调整**——不要臆造。`EnemyStateType` 若是 `EnemyBase` 的嵌套类型,则写 `EnemyBase.EnemyStateType.Controlled`。
- [ ] **Step 3: 刷新并确认编译零错误**
`unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(severity 'error') = 0。
- [ ] **Step 4: 提交**
```bash
git add Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef \
Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs.meta \
Assets/_Game/Scripts/Enemies/AIBrain.meta
git commit -m "feat(ai): EnemyBrainContext——EnemyBase子系统适配为BrainGraph能力接口"
```
---
## Task 3: `EnemyAiBrain` 组件
**Files:**
- Create: `Assets/_Game/Scripts/Enemies/AIBrain/EnemyAiBrain.cs`
- [ ] **Step 1: 创建 `EnemyAiBrain.cs`**
```csharp
using UnityEngine;
using BaseGames.AI;
namespace BaseGames.Enemies
{
/// <summary>
/// 把 BrainGraph 决策层挂到敌人上:按 id 取共享 AiGraph、构造 AiRuntime、逐帧推进。
/// 取代旧的 Opsive BehaviorTree 组件。
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(EnemyBase))]
public sealed class EnemyAiBrain : MonoBehaviour
{
[Tooltip("对应 [AiDefinition(id)] AI id E001")]
[SerializeField] string _definitionId;
EnemyBase _enemy;
EnemyBrainContext _context;
AiRuntime _runtime;
public string DefinitionId => _definitionId;
public string CurrentStateName => _runtime != null ? _runtime.CurrentStateName : "(none)";
public bool IsSuspended => _runtime != null && _runtime.IsSuspended;
public AiRuntime Runtime => _runtime;
void Awake()
{
_enemy = GetComponent<EnemyBase>();
if (string.IsNullOrEmpty(_definitionId))
{
// 根因暴露:漏配 id 直接报错,不静默兜底。
Debug.LogError($"EnemyAiBrain 未配置 _definitionId{name}", this);
enabled = false;
return;
}
var graph = AiDefinitionRegistry.GetGraph(_definitionId); // 不存在则抛异常
_context = new EnemyBrainContext(_enemy);
_runtime = new AiRuntime(graph, _context);
}
void Update()
{
if (_runtime == null) return;
float dt = Time.deltaTime;
_context.Refresh(dt);
_runtime.Tick(dt);
}
/// <summary>对象池复用时由 EnemyBase.OnSpawn 调用:回到 Entry、清临时态。</summary>
public void ResetBrain()
{
_context?.ResetScratch();
_runtime?.Reset();
}
/// <summary>信号入口,供 EnemyBase 转发死亡等事件到决策层。</summary>
public void Send(AiSignal signal) => _runtime?.Send(signal);
}
}
```
> 设计说明:本阶段每帧 Tick(不做 LOD 节流)——LOD/错峰调度属后续 AiScheduler 阶段。`AiRuntime.Reset()` 已清 Blackboard,但 `EnemyBrainContext` 的 `_lastKnown/_lostTimer` 另需 `ResetScratch()` 清(`AiRuntime` 不知道 context 内部字段)。
- [ ] **Step 2: 刷新并确认编译零错误**
`unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(error) = 0。
- [ ] **Step 3: 提交**
```bash
git add Assets/_Game/Scripts/Enemies/AIBrain/EnemyAiBrain.cs Assets/_Game/Scripts/Enemies/AIBrain/EnemyAiBrain.cs.meta
git commit -m "feat(ai): EnemyAiBrain 组件——承载 AiRuntime、逐帧推进、池复用重置、信号入口"
```
---
## Task 4: E001 的 AiScript 定义
**Files:**
- Create: `Assets/_Game/Scripts/Enemies/AIBrain/Ai/E001CaoZhiAi.cs`
- [ ] **Step 1: 创建 `E001CaoZhiAi.cs`**
```csharp
using BaseGames.AI;
namespace BaseGames.Enemies
{
/// <summary>
/// E001(草蛭)的 AI 定义:巡逻 → 追击(接触即伤害,无需攻击能力)→ 搜查 → 巡逻。
/// 数值目前用字面量常量;后续可外挂 SO 做热调。
/// </summary>
[AiDefinition("E001")]
public sealed class E001CaoZhiAi : AiScript
{
const float ContactRange = 1.2f; // 贴近判定(仅用于 Chase→Search 前的接近,接触伤害由 BodyContactDamage 触发)
const float LoseTimeout = 2.5f; // 丢失玩家判定
const float SearchTime = 3f; // 搜查后返回巡逻
protected override void Build(BrainBuilder b)
{
b.Entry("Patrol");
// 死亡:全局事件,终止决策
b.Global().To("Dead").OnEvent(AiSignal.Died);
b.State("Patrol")
.Tick(c => c.Mover.WalkRandom())
.To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer");
b.State("Chase")
.OnEnter(c => c.Mover.FacePlayer())
.Tick(c => c.Mover.MoveTo(c.Sensor.LastKnown)) // 冲向玩家,接触即由 BodyContactDamage 造成伤害
.To("Search").When(c => c.Sensor.LostFor(LoseTimeout), "LostFor");
b.State("Search")
.OnEnter(c => c.Mover.LookAround())
.To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer")
.To("Patrol").After(SearchTime);
b.State("Dead")
.OnEnter(c => c.Mover.Stop());
}
}
}
```
> `ContactRange` 常量暂未用于转换(接触伤害由碰撞触发);保留供后续若加"贴脸减速/停顿"用。如实现时发现 E001 需要一个显式攻击能力,再加 Combat 态与 `c.Combat.UseAbility(...)`(先 Read E001 prefab 的 Abilities 子节点确认真实 abilityId)。
- [ ] **Step 2: 刷新并确认编译零错误;确认注册表能发现 E001**
`unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(error) = 0。
`unity_execute_code`(port 7890) 执行断言:`BaseGames.AI.AiDefinitionRegistry.Has("E001")` 应为 true`GetGraph("E001").EntryState` 应为 `"Patrol"`
- [ ] **Step 3: 提交**
```bash
git add Assets/_Game/Scripts/Enemies/AIBrain/Ai/E001CaoZhiAi.cs Assets/_Game/Scripts/Enemies/AIBrain/Ai/E001CaoZhiAi.cs.meta \
Assets/_Game/Scripts/Enemies/AIBrain/Ai.meta
git commit -m "feat(ai): E001(草蛭) AiScript 定义——巡逻/追击/搜查(接触伤害杂兵)"
```
---
## Task 5: EnemyBase 移除 Opsive BT 集成 + 接 EnemyAiBrain
**Files:**
- Modify: `Assets/_Game/Scripts/Enemies/EnemyBase.cs`
这是本阶段最需谨慎的改动。**务必先完整 Read `EnemyBase.cs` 确认每个 `#if GRAPH_DESIGNER` 块与目标方法的真实当前内容与行号**(下面行号为探查快照,可能因前面提交而漂移)。逐块处理、每步后刷新确认零编译错误。
- [ ] **Step 1: 先 Read `EnemyBase.cs`,定位所有 `#if GRAPH_DESIGNER ... #endif` 块**(用 Grep `GRAPH_DESIGNER`)。预期位置:using(~:8-10)、LOD 间隔字段(~:60-72)、调试字段(~:135-137)、`BehaviorTree` 属性(~:257-259)、`SetAggroTickRate`(~:376-380)、`SetAiPhase` 内 BT 间隔 switch(~:471-483)、Awake 挂树(~:606-618)、Update tick(~:629-648)、Update 调试写(~:654-656)、BT 字段(~:697-702)、`StopBehaviorTree()`(~:709-714)、OnSpawn(~:807-809)、OnDespawn(~:823-825)。
- [ ] **Step 2: 删除所有 Opsive/BT 专属代码**
-`#if GRAPH_DESIGNER using Opsive...#endif`
-`_btIdleTickInterval``_btCombatTickInterval` 等 LOD 间隔字段与 `_dbg_BtTickInterval` 调试字段。
-`public BehaviorTree BehaviorTree => _behaviorTree;` 属性。
- 删 Awake 里 `#if GRAPH_DESIGNER … _behaviorTree = GetComponent<BehaviorTree>() … #endif` 整块。
- 删 Update 里 `#if GRAPH_DESIGNER … _behaviorTree.Tick() … #endif` 整块与其后调试写行。
-`_behaviorTree/_btManualMode/_btTickTimer/_btCurrentInterval` 字段。
-`StopBehaviorTree()` 方法**体内**的 BT 调用;见 Step 4 关于该 public 方法的处理。
- 删 OnSpawn/OnDespawn 里的 `#if GRAPH_DESIGNER … _behaviorTree… #endif` 块(OnSpawn 的替换见 Step 4)。
- `SetAggroTickRate(bool)`:删其 BT 间隔赋值;若方法体因此为空,保留为空方法体并加注释 `// LOD tick 速率控制迁移到后续 AiScheduler 阶段`(不要删方法,避免改动调用方)。
- `SetAiPhase(...)`:只删其中设置 `_btCurrentInterval` 的 switch**保留** `_currentAiPhase` 赋值、`OnAiPhaseChanged` 广播、阶段动画等其余逻辑。
- [ ] **Step 3: 加 EnemyAiBrain 引用与 Awake 获取**
- 加字段:`private EnemyAiBrain _brain;`
- 在 Awake 的子系统收集处(如 `_abilities.CollectFrom(gameObject);` 附近)加:`_brain = GetComponent<EnemyAiBrain>();`
- [ ] **Step 4: 死亡发信号 + 池复用重置**
-`PerformDeath()`(死亡结算处,原先调用 `StopBehaviorTree`/`ForceState(Dead)` 的地方)加:`_brain?.Send(BaseGames.AI.AiSignal.Died);`
- `StopBehaviorTree()` 现无 BT 可停:把方法体改为 `_brain?.Send(BaseGames.AI.AiSignal.Died);`(保留方法名,因 `EnemyDeathSequence` 等外部调用它;语义变为"通知决策层终止")。若 Step 4 已在 PerformDeath 直接 Send,则 `StopBehaviorTree` 保留同一 Send 以兼容既有调用点即可(幂等,重复 Died 事件不会二次转换)。
-`OnSpawn()`(原 `#if GRAPH_DESIGNER _behaviorTree?.StartBehavior(); #endif` 处)替换为:`_brain?.ResetBrain();`
- [ ] **Step 5: 逐步刷新并确认零编译错误**
每删/改一处后 `unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(error)。全部处理完应为 **0 error**。注意:删除 BT 字段后若有残留引用(如 debug inspector),一并清理。
- [ ] **Step 6: 跑全量 EditMode 测试确认无回归**
`unity_advanced_tool``unity_testing_run_tests`(EditMode) → `unity_testing_get_job`:应无失败(此改动不涉及 EditMode 测试,但确认未破坏编译面)。
- [ ] **Step 7: 提交**
```bash
git add Assets/_Game/Scripts/Enemies/EnemyBase.cs
git commit -m "refactor(ai): EnemyBase 移除 Opsive BT 集成,改接 EnemyAiBrain(Died信号/OnSpawn重置)"
```
---
## Task 6: 脚手架一键挂载 `EnemyAiBrain``SceneObjectPlacerTool`
**Files:**
- Modify: `Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs`
- [ ] **Step 1: 定位 `PlaceE001_CaoZhi` 里的行为树 TODO 提示行**
先 Read 该方法(~:439-530),找到 `report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。");`~:524)。
- [ ] **Step 2: 用真实挂载替换该提示行**
把该 `report.Add("★ …")` 行替换为(放在 `SetupPerceptionSystemSlots(...)` 之后、方法收尾之前):
```csharp
// BrainGraph AI:挂载决策组件并绑定定义 id(取代旧的 Opsive BehaviorTree
var brain = GetOrAddComponent<EnemyAiBrain>(go);
AssignString(brain, "_definitionId", "E001", report);
report.Add("✔ 已挂载 EnemyAiBrain 并绑定 AI 定义 id=E001BrainGraph)。");
```
> `EnemyAiBrain` 在 `BaseGames.Enemies``SceneObjectPlacerTool` 是 Editor 程序集,需确认 Editor asmdef 已引用 `BaseGames.Enemies`(放置工具本就 new/操作 EnemyBase,故必然已引用——若编译报找不到类型,检查 Editor asmdef references)。文件顶部按需加 `using BaseGames.Enemies;`。`AssignString` 为该文件内既有 private static 辅助(~:2333)。
- [ ] **Step 3: 刷新并确认编译零错误**
`unity_execute_menu_item`("Assets/Refresh") → `unity_get_compilation_errors`(error) = 0。
- [ ] **Step 4: 提交**
```bash
git add Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
git commit -m "feat(ai): 脚手架 PlaceE001 一键挂载 EnemyAiBrain 并绑定 id=E001"
```
---
## Task 7: 集成验证(脚手架一键 → PlayMode 驱动 → 无 BehaviorTree
**Files:** 无(经 MCP 驱动 Unity 验证;如需可加临时验证脚本但完成后删除)。
- [ ] **Step 1: 准备一个干净场景并确认活动场景**
用 MCP`unity_scene_new`(或打开一个测试场景)。**务必确认活动场景正确**(避免污染其他场景,见项目约束)。记录场景名。
- [ ] **Step 2: 脚手架一键放置 E001**
用 MCP `unity_execute_menu_item`(menuPath `"BaseGames/Scene/Place/Enemy E001 (草蛭)"`, port 7890)。
然后 `unity_scene_hierarchy` / `unity_gameobject_info` 找到新建的 `ENM_CaoZhi`
- [ ] **Step 3: 静态断言——挂了 EnemyAiBrain、绑了 id、且无 BehaviorTree**
`unity_component_get_properties`(或 `unity_gameobject_info`)确认 `ENM_CaoZhi`
-`EnemyAiBrain` 组件,其 `_definitionId == "E001"`
- **没有** `BehaviorTree`Opsive)组件。
- [ ] **Step 4: PlayMode 冒烟——决策层随玩家远近推进**
- 确保场景内有玩家(或用 MCP 在玩家可被 E001 感知的位置放一个带 Player 标签/层的对象;若测试场景无玩家系统,可退而验证 Step 5 的状态读取)。
- `unity_play_mode`(enter) → 等一帧。
- `unity_execute_code`(port 7890) 读取:`GameObject.Find("ENM_CaoZhi").GetComponent<BaseGames.Enemies.EnemyAiBrain>().CurrentStateName`
- 无玩家在感知范围时应为 `"Patrol"`(证明 AiRuntime 在 Tick、状态机在跑)。
- 让玩家进入 E001 感知范围(移动玩家 Transform 到近处),再读 `CurrentStateName` 应变为 `"Chase"`;移开并等待 `LoseTimeout` 后应变 `"Search"`
- `unity_play_mode`(exit)。
- [ ] **Step 5: 记录验证结论**
在计划文件或提交信息里记录:脚手架一键产出的 E001 由 BrainGraph 驱动、无 BehaviorTree 组件、状态随感知推进。若无玩家系统导致 Chase/Search 未能触发,至少确认 Patrol 态在跑 + 无 BehaviorTree + 注册表解析 E001 成功,并记录"完整行为待真实关卡验证"。
- [ ] **Step 6: 运行项目自检(合规)**
用 MCP `unity_execute_menu_item` 依次跑(若菜单存在):
- `BaseGames/Tools/Validation/Validate All ScriptableObjects`
- `BaseGames/Addressables/Validate Address Keys`
- `BaseGames/Tools/Maintenance/Physics2D Layer Matrix/Check`
记录无新增错误。
- [ ] **Step 7: 提交验证产物(若有)/收尾**
```bash
# 若 Step 1 新建了测试场景且需要保留,按规范命名保存;否则不提交场景。
git add -- <仅本任务需保留的文件>
git commit -m "test(ai): 集成验证——脚手架一键 E001 跑在 BrainGraph 上、无 BehaviorTree"
```
> ⚠️ 只提交本任务确需保留的文件;不要 `git add -A`(仓库有既存无关改动)。
---
## 后续(不属本阶段)
- LOD/错峰 tickAiScheduler)、能力编排 sugar、参数 SO 热调(把 E001 字面量迁到 SO)。
- 其余敌人(E002-E006)与 Boss(ChaoFeng,多阶段/加权技能/弱点)迁移;`CharacterWizardWindow` 若要显式选 AI 定义再加下拉。
- 全部敌人达到 parity 后:删 49 个 `BD_*``BaseGames.Enemies.AI` 去 Opsive 引用、移除 `com.opsive.*` 包与 `GRAPH_DESIGNER` define。
- 交付面:MermaidExporter / AiDebugService / 运行时调试器窗口。
---
## 自检记录(writing-plans self-review
- **Spec 覆盖**:注册表(§7.1)、上下文适配(§4/单类实现,用户已选)、EnemyAiBrain 组件(§7.1)、EnemyBase 去 BT+信号+池复用重置(§7.1/§3.6/§3.8)、脚手架接线(§7.3/§2)、E001 试点(§8)、集成验证(§11.1)。LOD/编排/参数SO/其余敌人/删BD 明确列入后续。
- **占位扫描**:无 TBD。ContactRange 常量未用于转换已注明用途;ability 相关按"接触伤害"简化,注明若需攻击能力再加。
- **类型一致性**`AiDefinitionRegistry.GetGraph/Has/Ids``EnemyBrainContext`(实现 IAiContext+4 接口)、`EnemyAiBrain.CurrentStateName/ResetBrain/Send/DefinitionId``AiRuntime.Reset/Send/CurrentStateName/IsSuspended``AiScript.GetOrBuildGraph``[AiDefinition]` 跨任务一致。
- **风险点**Task 5EnemyBase 手术)最需谨慎——已要求先 Read 核对真实行号、逐块刷新验证;Task 2 的 EnemyBase 成员名以真实签名为准(要求核对不臆造);Task 7 依赖测试场景有玩家,已给降级验证路径。