diff --git a/Docs/superpowers/plans/2026-07-03-braingraph-phase1-core-runtime.md b/Docs/superpowers/plans/2026-07-03-braingraph-phase1-core-runtime.md index eebd874b..2848869b 100644 --- a/Docs/superpowers/plans/2026-07-03-braingraph-phase1-core-runtime.md +++ b/Docs/superpowers/plans/2026-07-03-braingraph-phase1-core-runtime.md @@ -542,9 +542,9 @@ namespace BaseGames.Tests.EditMode.AI b.Entry("Patrol"); b.Global().To("Dead").OnEvent(AiSignal.Died); b.State("Patrol") - .To("Chase").When(c => c.Sensor.SeesPlayer()); + .To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer"); b.State("Chase") - .To("Patrol").When(c => c.Sensor.LostFor(2f)); + .To("Patrol").When(c => c.Sensor.LostFor(2f), "LostFor(2s)"); b.State("Dead"); return b.Build(); } @@ -565,11 +565,22 @@ namespace BaseGames.Tests.EditMode.AI } [Test] - public void Build_CapturesConditionLabelFromExpressionText() + public void Build_UsesExplicitConditionLabel() { var g = BuildSample(); var t = g.GetState("Patrol").Transitions[0]; - StringAssert.Contains("SeesPlayer", t.Label); + Assert.AreEqual("SeesPlayer", t.Label); + } + + [Test] + public void Build_UnlabeledCondition_FallsBackToCond() + { + var b = new BrainBuilder(); + b.Entry("A"); + b.State("A").To("B").When(c => true); + b.State("B"); + var g = b.Build(); + Assert.AreEqual("cond", g.GetState("A").Transitions[0].Label); } [Test] @@ -639,7 +650,6 @@ namespace BaseGames.AI ```csharp using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; namespace BaseGames.AI { @@ -716,11 +726,14 @@ namespace BaseGames.AI internal TransitionBuilder(BrainBuilder owner, StateBuilder sb, AiState state, string target) { _owner = owner; _stateBuilder = sb; _state = state; _target = target; } - /// 条件转换。label 默认由 [CallerArgumentExpression] 抓取 cond 的源码文本。 - public StateBuilder When(Func cond, - [CallerArgumentExpression("cond")] string label = null) + /// + /// 条件转换。label 为可读标签(用于 Mermaid 边 / trace 触发原因), + /// C# 9 环境下需显式传入;不传则回退为 "cond"。 + /// (升级到 C# 10 后可改用 [CallerArgumentExpression] 自动抓取源码文本。) + /// + public StateBuilder When(Func cond, string label = null) { - _state.Transitions.Add(Transition.OnCondition(_target, cond, label)); + _state.Transitions.Add(Transition.OnCondition(_target, cond, label ?? "cond")); return _stateBuilder; } @@ -754,10 +767,9 @@ namespace BaseGames.AI return this; } - public GlobalBuilder When(Func cond, - [CallerArgumentExpression("cond")] string label = null) + public GlobalBuilder When(Func cond, string label = null) { - _owner.AddGlobal(Transition.OnCondition(_pendingTarget, cond, label)); + _owner.AddGlobal(Transition.OnCondition(_pendingTarget, cond, label ?? "cond")); return this; } } @@ -767,19 +779,7 @@ namespace BaseGames.AI > 注:`After()` 引用了 `AiRuntime.TimeInState(c)`(Task 5 定义的静态辅助,通过 context 取当前 runtime 的状态计时器)。若执行到本任务时 `AiRuntime` 尚未定义,先临时把 `After` 的实现体改为 `throw new NotImplementedException()` 以通过编译,Task 5 完成后回填(回填是同一函数体,非占位)。测试 `BrainBuilderTests` 不覆盖 `After`,不受影响。 > -> **CallerArgumentExpression 兼容性**:需 C# 10。若项目 Unity 版本的语言级别不支持,添加一个 polyfill:新建 `Assets/_Game/Scripts/AI/CallerArgumentExpressionAttribute.cs`: -> ```csharp -> #if !NET5_0_OR_GREATER -> namespace System.Runtime.CompilerServices { -> [AttributeUsage(AttributeTargets.Parameter)] -> internal sealed class CallerArgumentExpressionAttribute : Attribute { -> public CallerArgumentExpressionAttribute(string parameterName) => ParameterName = parameterName; -> public string ParameterName { get; } -> } -> } -> #endif -> ``` -> 若 polyfill 后编译器仍不填充(语言版本 < 10),标签会退化为默认值 —— 此时把 `Build_CapturesConditionLabelFromExpressionText` 标记为 `[Ignore("需 C# 10")]` 并在第 6 阶段处理,不阻塞核心。 +> **标签方案(C# 9 环境,Unity 2022.3)**:转换的可读标签走**显式可选字符串**(`When(cond, "SeesPlayer")`)。声明时建议给关键条件转换传标签,使 Mermaid 边 / trace 触发原因可读;不传则回退为 `"cond"`。**不使用 `[CallerArgumentExpression]`**(那是 C# 10 特性,本项目 Unity 2022.3 用 C# 9,编译器不会填充)。将来升级语言级别后可无痛切换为自动抓取。 - [ ] **Step 5: 运行测试确认通过** @@ -891,11 +891,11 @@ namespace BaseGames.Tests.EditMode.AI b.Global().To("Dead").OnEvent(AiSignal.Died); b.State("Patrol") .Tick(c => c.Mover.WalkRandom()) - .To("Chase").When(c => c.Sensor.SeesPlayer()); + .To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer"); b.State("Chase") .OnEnter(c => c.Mover.FacePlayer()) .Tick(c => c.Mover.MoveTo(c.Sensor.LastKnown)) - .To("Search").When(c => c.Sensor.LostFor(2f)); + .To("Search").When(c => c.Sensor.LostFor(2f), "LostFor(2s)"); b.State("Search") .To("Patrol").After(3f); b.State("Dead"); @@ -1307,4 +1307,4 @@ git commit -m "chore(ai): BrainGraph 第1阶段核心运行时收尾——全测 - **Spec 覆盖**:本阶段对应 spec §3.1(共享图)/§3.2(轻量运行时)/§3.3(条件+事件转换)/§3.6(IsControllable门)/§3.8(池复用重置、无实例捕获约束经 IAiContext 落实)。§3.4/§3.5黑板已建/§3.7/§4完整/§6/§7/§8 明确列入后续阶段路线图,无遗漏。 - **占位扫描**:无 TBD/TODO 占位;`After` 的临时抛异常 + Task 5 回填是明确的两步实现(非占位),已注明回填内容与顺序。 - **类型一致性**:`IAiContext.Sensor/Mover/Combat/Vitals/Blackboard`、`AiRuntime.CurrentStateName/IsSuspended/Trace/Send/Reset/Tick`、`Transition.OnCondition/OnEvent/IsEvent/Label`、`BrainBuilder.Entry/State/Global/Build`、`StateBuilder.OnEnter/Tick/OnExit/To`、`TransitionBuilder.When/OnEvent/After` 跨任务命名一致。 -- **已知风险**:CallerArgumentExpression 需 C# 10,已给 polyfill + 降级方案(Task 4 Step 4 注)。 +- **环境适配**:Unity 2022.3 = C# 9。转换标签走显式可选字符串(不用 C# 10 的 CallerArgumentExpression);`After` 用状态计时器、事件转换用 `AiSignal`,均 C# 9 兼容。 diff --git a/Docs/superpowers/specs/2026-07-03-enemy-ai-framework-design.md b/Docs/superpowers/specs/2026-07-03-enemy-ai-framework-design.md index 30635bdd..c5a46353 100644 --- a/Docs/superpowers/specs/2026-07-03-enemy-ai-framework-design.md +++ b/Docs/superpowers/specs/2026-07-03-enemy-ai-framework-design.md @@ -122,7 +122,7 @@ Behavior Designer 的行为树是**手绘的二进制图资产**: ### 3.8 多实例复用与对象池(同一场景多个相同敌人) -- **共享 `AiGraph`(每敌人类型 1 份,flyweight)**:状态图结构、转换、能力编排、条件 lambda、`[CallerArgumentExpression]` 抓到的条件文本标签,全部只构建一次、由该类型所有实例共享。场景放 N 个相同杂兵 → 结构零 per-instance 分配、无重复 GC、缓存友好。 +- **共享 `AiGraph`(每敌人类型 1 份,flyweight)**:状态图结构、转换、能力编排、条件 lambda、显式条件标签字符串,全部只构建一次、由该类型所有实例共享。场景放 N 个相同杂兵 → 结构零 per-instance 分配、无重复 GC、缓存友好。 - **每实例 `AiRuntime`(N 份)**:仅当前状态指针、`Blackboard`、计时器、`AbilitySequence` 游标随实例独立演化。 - **无实例捕获约束**:声明里的条件/动作 lambda 只能通过参数 `c`(上下文)访问实例,**不得闭包捕获具体敌人实例**——这是 lambda 能挂在共享图上的前提。由 `BrainBuilder` 约定保证(后续可加 Roslyn 分析器强校验)。 - **对象池重置**:`EnemyBase : IPoolable`(池复用走 `ForceStateRespawn`)。敌人回池再生时,`EnemyAiBrain` 必须重置 `AiRuntime`:回到 Entry 状态、清空黑板/计时器/游标、清空 trace。**共享 `AiGraph` 不重置**(无状态)。挂进 `IPoolable` 的回收/取出回调。 @@ -168,33 +168,33 @@ public sealed class BasicMeleeGruntAi : EnemyAiScript // 注意:普通受击/硬直/击飞的"让位"由 IsControllable 门自动处理,无需在此声明。 // 父层事件转换只用于决策层需要的、超出物理动画的额外行为响应。 b.Global() - .To("Dead").OnEvent(AiSignal.Died) // 终止决策 - .To("Enrage").When(c => c.Vitals.HpBelow(c.P.EnrageHpRatio)); // 低血狂暴(示例) + .To("Dead").OnEvent(AiSignal.Died) // 终止决策 + .To("Enrage").When(c => c.Vitals.HpBelow(c.P.EnrageHpRatio), "HpBelow(enrage)"); // 低血狂暴(示例) b.State("Patrol") .Tick(c => c.Mover.WalkRandom()) - .To("Chase").When(c => c.Sensor.SeesPlayer()); + .To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer"); b.State("Chase") .Tick(c => c.Mover.MoveTo(c.Blackboard.LastKnown)) - .To("Combat").When(c => c.Sensor.InRange(c.P.MeleeRange)) - .To("Search").When(c => c.Sensor.LostFor(c.P.LoseLinkTimeout)); + .To("Combat").When(c => c.Sensor.InRange(c.P.MeleeRange), "InRange(melee)") + .To("Search").When(c => c.Sensor.LostFor(c.P.LoseLinkTimeout), "LostFor(timeout)"); b.State("Combat") .OnEnter(c => c.Mover.FacePlayer()) .Do(Seq.Select( // 按 SO 的 preferredRange/priority/exclusion 选招并等待其结束 - Ability("slash").When(c => c.Sensor.InRange(c.P.MeleeRange)))) - .To("Chase").When(c => !c.Sensor.InRange(c.P.MeleeRange) && c.Combat.NoAbilityRunning); + Ability("slash").When(c => c.Sensor.InRange(c.P.MeleeRange), "InRange(melee)"))) + .To("Chase").When(c => !c.Sensor.InRange(c.P.MeleeRange) && c.Combat.NoAbilityRunning, "outOfMelee"); b.State("Search") .OnEnter(c => c.Mover.LookAround()) - .To("Chase").When(c => c.Sensor.SeesPlayer()) + .To("Chase").When(c => c.Sensor.SeesPlayer(), "SeesPlayer") .To("Patrol").After(c => c.P.InvestigateDuration); } } ``` -- 转换条件的可读标签由 `[CallerArgumentExpression]` 自动从 lambda 源码文本抓取(可选覆写)。 +- 转换条件的可读标签走**显式可选字符串**(`When(cond, "SeesPlayer")`):本项目 Unity 2022.3 = C# 9,不用 C# 10 的 `[CallerArgumentExpression]`;不传标签则回退 `"cond"`。将来升级语言级别可无痛切换为自动抓取。 - `c.P.*` = 参数视图,解析自 `EnemyStatsSO` / 能力 SO。 --- @@ -203,7 +203,7 @@ public sealed class BasicMeleeGruntAi : EnemyAiScript ### 6.1 Mermaid/dot 导出器 -- 纯函数 `AiGraph → stateDiagram-v2` 文本;层级子机 → composite state;边标签取自 `[CallerArgumentExpression]` 抓到的条件文本 / 事件枚举名。 +- 纯函数 `AiGraph → stateDiagram-v2` 文本;层级子机 → composite state;边标签取自转换的显式标签字符串 / 事件枚举名。 - 入口:菜单 `BaseGames/AI/Export Graph` + 静态方法(打印 Console,供 MCP `unity_execute_code` 抓);文件写 `Docs/AI/.md`。 ### 6.2 运行时实时调试器(Editor 窗口,仅编辑器)