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 窗口,仅编辑器)