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:
@@ -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.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`**
|
||||
|
||||
```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 提供共享 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: 提交**
|
||||
|
||||
```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=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: 提交**
|
||||
|
||||
```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/错峰 tick(AiScheduler)、能力编排 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 5(EnemyBase 手术)最需谨慎——已要求先 Read 核对真实行号、逐块刷新验证;Task 2 的 EnemyBase 成员名以真实签名为准(要求核对不臆造);Task 7 依赖测试场景有玩家,已给降级验证路径。
|
||||
Reference in New Issue
Block a user