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

29 KiB
Raw Blame History

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 子系统包成能力接口的上下文、承载 AiRuntimeEnemyAiBrain 组件、E001 的 AiScript 定义,移除 EnemyBase 的 Opsive BT 集成,并在 SceneObjectPlacerTool 里自动挂载/绑定。

Architecture: EnemyAiBrain : MonoBehaviour_definitionId(string)AwakeAiDefinitionRegistry.GetGraph(id) 取每类型共享的不可变 AiGraph,构造 EnemyBrainContext(实现 IAiContext+四能力接口,委托 EnemyBase 子系统)与 AiRuntimeUpdateTick。EnemyBase 只在死亡处 Send(AiSignal.Died)OnSpawnResetBrain(),受击让位由 IsControllable 门自动处理。脚手架把 EnemyAiBrain 挂上并 AssignString(_definitionId,"E001")

Tech Stack: Unity 2022.3C# 9, Unity Test Framework (EditMode), asmdef, Addressables, MCP(编译/测试/PlayMode 验证)。

依赖: BrainGraph 第 1 阶段核心运行时已合并 master(BaseGames.AIAiGraph/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)——接触即伤害,无需攻击能力。
  • EnemyBaseAssets/_Game/Scripts/Enemies/EnemyBase.cs)暴露:IsPlayerVisible()IsPlayerInRange(float)PlayerTransformNav(IPathAgent,含 IsMoving/WalkToRandom)、MoveTo(Vector2)FacePlayer()StopMovement()BeginLookAround()Abilities(EnemyAbilityRegistry)、IsAliveCurrentState(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.AIAssets/_Game/Scripts/AI/):

  • AiDefinitionRegistry.cs — 反射按 id 提供共享 AiGraph。

新增(BaseGames.EnemiesAssets/_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.csPlaceE001_CaoZhiEnemyAiBrain + 绑 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(经 MCPunity_advanced_toolunity_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 提供共享 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: 提交
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 属性/方法名与上面不符(如 NavIsMoving),先 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: 提交
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") 应为 trueGetGraph("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(用 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_toolunity_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: 脚手架一键挂载 EnemyAiBrainSceneObjectPlacerTool

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(...) 之后、方法收尾之前):

            // BrainGraph AI:挂载决策组件并绑定定义 id(取代旧的 Opsive BehaviorTree
            var brain = GetOrAddComponent<EnemyAiBrain>(go);
            AssignString(brain, "_definitionId", "E001", report);
            report.Add("✔ 已挂载 EnemyAiBrain 并绑定 AI 定义 id=E001BrainGraph)。");

EnemyAiBrainBaseGames.EnemiesSceneObjectPlacerTool 是 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: 准备一个干净场景并确认活动场景

用 MCPunity_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"

  • 没有 BehaviorTreeOpsive)组件。

  • 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/错峰 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/IdsEnemyBrainContext(实现 IAiContext+4 接口)、EnemyAiBrain.CurrentStateName/ResetBrain/Send/DefinitionIdAiRuntime.Reset/Send/CurrentStateName/IsSuspendedAiScript.GetOrBuildGraph[AiDefinition] 跨任务一致。
  • 风险点Task 5EnemyBase 手术)最需谨慎——已要求先 Read 核对真实行号、逐块刷新验证;Task 2 的 EnemyBase 成员名以真实签名为准(要求核对不臆造);Task 7 依赖测试场景有玩家,已给降级验证路径。