29 KiB
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.3(C# 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
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
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace BaseGames.AI
{
/// <summary>
/// 反射收集所有 [AiDefinition] 的 AiScript 子类,按 id 提供共享 AiGraph(flyweight)。
/// 同一 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: PASS(4 项)。全量测试无失败。
- Step 5: 提交
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
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),先 ReadEnemyBase.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: 提交
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
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: 提交
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
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: 提交
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块(用 GrepGRAPH_DESIGNER)。预期位置:using(:8-10)、LOD 间隔字段(:60-72)、调试字段(:135-137)、:257-259)、BehaviorTree属性(SetAggroTickRate(:376-380)、:471-483)、Awake 挂树(SetAiPhase内 BT 间隔 switch(: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: 提交
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),找到 :524)。report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。");(
- Step 2: 用真实挂载替换该提示行
把该 report.Add("★ …") 行替换为(放在 SetupPerceptionSystemSlots(...) 之后、方法收尾之前):
// BrainGraph AI:挂载决策组件并绑定定义 id(取代旧的 Opsive BehaviorTree)
var brain = GetOrAddComponent<EnemyAiBrain>(go);
AssignString(brain, "_definitionId", "E001", report);
report.Add("✔ 已挂载 EnemyAiBrain 并绑定 AI 定义 id=E001(BrainGraph)。");
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: 提交
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: 提交验证产物(若有)/收尾
# 若 Step 1 新建了测试场景且需要保留,按规范命名保存;否则不提交场景。
git add -- <仅本任务需保留的文件>
git commit -m "test(ai): 集成验证——脚手架一键 E001 跑在 BrainGraph 上、无 BehaviorTree"
⚠️ 只提交本任务确需保留的文件;不要
git add -A(仓库有既存无关改动)。
后续(不属本阶段)
- LOD/错峰 tick(AiScheduler)、能力编排 sugar、参数 SO 热调(把 E001 字面量迁到 SO)。
- 其余敌人(E002-E006)与 Boss(ChaoFeng,多阶段/加权技能/弱点)迁移;
CharacterWizardWindow若要显式选 AI 定义再加下拉。 - 全部敌人达到 parity 后:删 49 个
BD_*、BaseGames.Enemies.AI去 Opsive 引用、移除com.opsive.*包与GRAPH_DESIGNERdefine。 - 交付面: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 5(EnemyBase 手术)最需谨慎——已要求先 Read 核对真实行号、逐块刷新验证;Task 2 的 EnemyBase 成员名以真实签名为准(要求核对不臆造);Task 7 依赖测试场景有玩家,已给降级验证路径。