Files
zeling_v2/Docs_Dev/superpowers/plans/2026-07-03-braingraph-phase-enemy-integration.md
T
2026-07-21 10:22:43 +08:00

635 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 依赖测试场景有玩家,已给降级验证路径。