docs(enemy): 计划新增 T5b——BrainBuilder.DeclareState 防模块间状态名冲突
契约质量审查提出:模块化后 State() 的"有则取"语义使两个独立模块 取同名状态时后者静默覆盖前者回调。与 T1 的 RequireState 是同一类 失败的两面,趁 0 个模块实现时补代价最低。附带补充交战/死亡层 "为何单阶段 Build 足够"的不变量说明。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -680,6 +680,130 @@ git commit -m "feat(enemy): 未发现层/交战层/死亡层三个模块契约"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Task 5b: `BrainBuilder.DeclareState` 防状态名冲突
|
||||||
|
|
||||||
|
> 本任务是执行 T5 后由契约质量审查提出并采纳的追加项,非原稿内容。
|
||||||
|
>
|
||||||
|
> **风险来由**:`BrainBuilder.State(name)` 是「有则取、无则建」。改造前 `PerceptionStateMachine`
|
||||||
|
> 是单个函数独占全部状态名,撞名不可能发生。改成「策划在下拉里自由组合三层独立模块」之后,
|
||||||
|
> 两个不同作者的模块取了同一个状态名(或某模块内部态叫 `Alert`,撞上骨架保留的那个),
|
||||||
|
> 后声明者的 `.OnEnter/.Tick/.OnExit` 会**静默覆盖**前者 —— 敌人跑错行为且零报错。
|
||||||
|
>
|
||||||
|
> 这与 T1 的 `RequireState` 是同一类失败的两面:`RequireState` 守「挂边到未声明的态」,
|
||||||
|
> `DeclareState` 守「重复声明同一个态」。趁只有 0 个模块实现时补,代价最低。
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `Assets/_Game/Scripts/AI/BrainBuilder.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/AIBrain/Modules/AiStateFragments.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/AIBrain/Modules/IEngagementModule.cs`
|
||||||
|
- Modify: `Assets/_Game/Scripts/Enemies/AIBrain/Modules/IDeathModule.cs`
|
||||||
|
- Test: `Assets/Tests/EditMode/AI/BrainBuilderTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败测试**
|
||||||
|
|
||||||
|
在 `BrainBuilderTests.cs` 类体内追加:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Test]
|
||||||
|
public void DeclareState_Throws_OnDuplicateName()
|
||||||
|
{
|
||||||
|
var b = new BrainBuilder();
|
||||||
|
b.DeclareState("Dup");
|
||||||
|
var ex = Assert.Throws<System.InvalidOperationException>(() => b.DeclareState("Dup"));
|
||||||
|
StringAssert.Contains("Dup", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeclareState_ReturnsUsableBuilder_OnFirstDeclare()
|
||||||
|
{
|
||||||
|
var b = new BrainBuilder();
|
||||||
|
b.Entry("A");
|
||||||
|
b.DeclareState("A").To("B").When(c => true, "go");
|
||||||
|
b.DeclareState("B");
|
||||||
|
Assert.DoesNotThrow(() => b.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void State_StaysLenient_ForAttachingToDeclaredState()
|
||||||
|
{
|
||||||
|
// State() 仍是"有则取"——骨架要给模块已声明的态挂升级边,靠的就是这个。
|
||||||
|
var b = new BrainBuilder();
|
||||||
|
b.Entry("A");
|
||||||
|
b.DeclareState("A");
|
||||||
|
Assert.DoesNotThrow(() => b.State("A").To("A").When(c => false, "noop"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行测试确认失败**
|
||||||
|
|
||||||
|
运行 `BrainBuilderTests`。期望:编译错误 `'BrainBuilder' does not contain a definition for 'DeclareState'`。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现 `DeclareState`**
|
||||||
|
|
||||||
|
在 `BrainBuilder.cs` 的 `RequireState` 之后插入:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// 首次声明一个状态。名字已存在即抛——防止两个独立模块取了同名状态时,
|
||||||
|
/// 后者的 OnEnter/Tick/OnExit 静默覆盖前者(敌人跑错行为且零报错)。
|
||||||
|
/// 只有"首次声明行为"的调用点用它;给已声明的态挂边仍用 State()。
|
||||||
|
/// </summary>
|
||||||
|
public StateBuilder DeclareState(string name)
|
||||||
|
{
|
||||||
|
if (_states.ContainsKey(name))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"BrainBuilder: 状态 '{name}' 已被声明过。两个模块取了同名状态会互相覆盖回调——" +
|
||||||
|
"请给其中一个换个不冲突的名字。");
|
||||||
|
return State(name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 让四个建态原语走 `DeclareState`**
|
||||||
|
|
||||||
|
`AiStateFragments.cs` 中,把四个原语里的 `b.State(state)` 全部换成 `b.DeclareState(state)`
|
||||||
|
(`Locomotion` / `Ability` / `AbilityOnce` / `Terminal` 各一处)。私有辅助方法不动。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 给交战层 / 死亡层契约补一条不变量说明**
|
||||||
|
|
||||||
|
`IEngagementModule.cs` 的接口 XML 注释末尾追加一段:
|
||||||
|
|
||||||
|
```
|
||||||
|
/// 为何本层只需单阶段 Build(而未发现层要拆 Declare/Link):骨架只把边**指向**
|
||||||
|
/// EntryState,从不在本模块自己的状态上挂边,所以 Build 内声明的边不会被外部抢占。
|
||||||
|
/// 将来若修改骨架、使其向交战态挂边,必须先回来重新审视本契约。
|
||||||
|
```
|
||||||
|
|
||||||
|
`IDeathModule.cs` 的接口 XML 注释末尾追加:
|
||||||
|
|
||||||
|
```
|
||||||
|
/// 同 IEngagementModule:骨架只把全局 Died 边指向 EntryState,不在死亡链内部挂边,
|
||||||
|
/// 故单阶段 Build 足够。
|
||||||
|
```
|
||||||
|
|
||||||
|
并在 `IUnawareModule.cs` 的 `Entry` 成员注释上补一句区分:
|
||||||
|
|
||||||
|
```
|
||||||
|
/// <summary>图入口(出生态)。注意与交战/死亡层的 EntryState 不同:
|
||||||
|
/// 本属性会成为整张图唯一的 BrainBuilder.Entry,而 EntryState 只是别处 To() 的目标。</summary>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: 运行测试确认通过**
|
||||||
|
|
||||||
|
运行 `BrainBuilderTests` 与 `AiStateFragmentsTests`,再跑全量 EditMode。
|
||||||
|
期望:新增 3 个测试,全量从 222 变为 225,0 失败。
|
||||||
|
|
||||||
|
> 若有既有测试因重复声明而失败,**停下报告** —— 那说明框架里本来就存在重复声明,
|
||||||
|
> 是真问题,不要靠把 `DeclareState` 改宽松来绕过。
|
||||||
|
|
||||||
|
- [ ] **Step 7: 提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add Assets/_Game/Scripts/AI/BrainBuilder.cs Assets/_Game/Scripts/Enemies/AIBrain/Modules Assets/Tests/EditMode/AI/BrainBuilderTests.cs
|
||||||
|
git commit -m "feat(ai): BrainBuilder.DeclareState——重复声明状态即报错,防模块间同名静默覆盖"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Task 6: 未发现层三个模块
|
## Task 6: 未发现层三个模块
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
|||||||
Reference in New Issue
Block a user