docs(enemy): 记录 EditMode 不触发 Awake 的坑,并分派两处越界发现
执行须知第 4 条:EditMode 下 AddComponent 不调用 Awake(Task 4 实测证实)。 后果是 EnemyAbilityBase._enemy 留空使 CanUse 恒假——依赖它的断言在修复前 也会通过,成为永远不会失败的测试。做法是给测试子类加 RunAwake() 显式触发, 不要反射直写字段。 Task 5 追加:IAttackCandidate 的 CanUse 注释补上 enabled 这一维。 Task 21 追加:EnemyQuotaManager._maxActiveBehaviorTrees 改名须带 FormerlySerializedAs,否则丢场景配置值。
This commit is contained in:
@@ -53,6 +53,16 @@ Unity 只对**编译失败**的程序集给出完整诊断集。`ChargeAbility.c
|
|||||||
**3. MCP 测试运行器忽略过滤参数。** `unity_testing_run_tests` 的 `testFilter` / `filter` 无效,
|
**3. MCP 测试运行器忽略过滤参数。** `unity_testing_run_tests` 的 `testFilter` / `filter` 无效,
|
||||||
每次都跑整个 EditMode 套件。这不影响正确性(等于免费拿到全量回归),但别把全量结果误读成过滤结果。
|
每次都跑整个 EditMode 套件。这不影响正确性(等于免费拿到全量回归),但别把全量结果误读成过滤结果。
|
||||||
|
|
||||||
|
**4. ⚠️ EditMode 下 `AddComponent` 不触发 `Awake`。**(Task 4 实测证实)
|
||||||
|
普通 MonoBehaviour 在编辑模式下 `AddComponent` 后 `Awake` **不会被调用**。
|
||||||
|
后果:任何依赖 `Awake` 解析引用的成员在 EditMode 测试里都是未初始化状态。
|
||||||
|
具体地,`EnemyAbilityBase._enemy` 会留空,使 `CanUse` **恒为 false**——
|
||||||
|
断言"禁用后不可用"会在修复前也通过,是个永远不会失败的测试。
|
||||||
|
|
||||||
|
做法:给测试用的子类加一个 `public void RunAwake() => Awake();` 显式触发,
|
||||||
|
**不要**用反射直写字段——前者跑的是真实的 `GetComponentInParent` 解析路径,后者是伪造结果。
|
||||||
|
只检查 `enabled` 标志本身(如阶段门的启停断言)则不需要 `Awake`。
|
||||||
|
|
||||||
### 项目硬约束
|
### 项目硬约束
|
||||||
|
|
||||||
1. **禁止下游兜底**(CLAUDE.md 第 6 条):不要用 `?? 默认值` / `try-catch` / `if(null) return` 掩盖上游漏配。缺配置就 `Debug.LogError` 或抛异常。
|
1. **禁止下游兜底**(CLAUDE.md 第 6 条):不要用 `?? 默认值` / `try-catch` / `if(null) return` 掩盖上游漏配。缺配置就 `Debug.LogError` 或抛异常。
|
||||||
@@ -729,9 +739,15 @@ namespace BaseGames.Tests.EditMode.Enemies
|
|||||||
[Test]
|
[Test]
|
||||||
public void DisabledAbility_IsNotUsable()
|
public void DisabledAbility_IsNotUsable()
|
||||||
{
|
{
|
||||||
|
// 必须先挂 EnemyBase:否则 _enemy==null 会让 CanUse 恒假,
|
||||||
|
// 这条断言在修复前也会"通过",成为一个永远不会失败的测试。
|
||||||
var go = new GameObject("ability-host");
|
var go = new GameObject("ability-host");
|
||||||
|
go.AddComponent<EnemyBase>(); // 裸 EnemyBase 的 _currentState=Controlled → IsAlive 为真
|
||||||
var ab = go.AddComponent<StubAbility>();
|
var ab = go.AddComponent<StubAbility>();
|
||||||
|
|
||||||
|
Assert.IsTrue(ab.CanUse,
|
||||||
|
"前提:启用且宿主存活时应可用——否则下一条断言分不清是 enabled 还是别的门在起作用");
|
||||||
|
|
||||||
ab.enabled = false;
|
ab.enabled = false;
|
||||||
Assert.IsFalse(ab.CanUse, "禁用的能力组件不可用——否则选招器会选中一个启不动协程的招");
|
Assert.IsFalse(ab.CanUse, "禁用的能力组件不可用——否则选招器会选中一个启不动协程的招");
|
||||||
|
|
||||||
@@ -741,14 +757,15 @@ namespace BaseGames.Tests.EditMode.Enemies
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> `using BaseGames.Enemies;` 需一并加入 using 区(`EnemyBase` 在该命名空间)。
|
||||||
|
> `EnemyBase.Awake` 末尾的 `_stats.Initialize(_statsSO)` 会在裸物体上 NRE,
|
||||||
|
> 但那发生在所有组件解析之后,不影响本测试;`LogAssert.ignoreFailingMessages` 已覆盖噪声。
|
||||||
|
|
||||||
- [ ] **Step 2: 跑测试确认失败**
|
- [ ] **Step 2: 跑测试确认失败**
|
||||||
|
|
||||||
Test Runner → EditMode → `BossPhaseAbilityGateTests.DisabledAbility_IsNotUsable`。
|
Test Runner → EditMode → `BossPhaseAbilityGateTests.DisabledAbility_IsNotUsable`。
|
||||||
期望:FAIL——`CanUse` 当前不看 `enabled`,返回 true。
|
期望:**第一条断言通过、第二条 FAIL**——`CanUse` 当前不看 `enabled`,禁用后仍返回 true。
|
||||||
|
若两条都过,说明测试没测到 `enabled` 这一维,停下来修测试再继续。
|
||||||
> 若因 `StubAbility` 上无 `EnemyBase` 导致 `_enemy == null` 使 `CanUse` 已为 false 而测试假通过:
|
|
||||||
> 给 `go` 先 `AddComponent<EnemyBase>()`,再断言 `ab.enabled = true` 时 `CanUse` 为 true、
|
|
||||||
> `false` 时为 false 的对比,确保断言真正测到 `enabled` 这一维。
|
|
||||||
|
|
||||||
- [ ] **Step 3: `CanUse` 纳入 `enabled`**
|
- [ ] **Step 3: `CanUse` 纳入 `enabled`**
|
||||||
|
|
||||||
@@ -792,7 +809,15 @@ StartCoroutine 必然失败的招。这也是阶段门(按阶段启停能力
|
|||||||
**Files:**
|
**Files:**
|
||||||
- Create: `Assets/_Game/Scripts/Enemies/Boss/BossPhaseAbilityGate.cs`
|
- Create: `Assets/_Game/Scripts/Enemies/Boss/BossPhaseAbilityGate.cs`
|
||||||
- Modify: `Assets/_Game/Scripts/Enemies/Boss/BossBase.cs`
|
- Modify: `Assets/_Game/Scripts/Enemies/Boss/BossBase.cs`
|
||||||
- Test: `Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs`
|
- Modify: `Assets/_Game/Scripts/Enemies/Abilities/IAttackCandidate.cs`(一行文档修正,见下)
|
||||||
|
- Test: `Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs`(Task 4 已建,本 Task 追加三条)
|
||||||
|
|
||||||
|
> **前置说明**:`StubAbility` 与 `_host` / `TearDown` 骨架在 Task 4 已经建好,含
|
||||||
|
> `public void RunAwake() => Awake();`(EditMode 不自动跑 `Awake`,见执行须知第 4 条)。
|
||||||
|
> 本 Task 的三条测试只断言 `enabled` 标志本身,**不需要** `RunAwake`。
|
||||||
|
>
|
||||||
|
> **顺带修正**:`IAttackCandidate.cs` 第 9 行把 `CanUse` 注释为 `// 未冷却 + 未运行 + 存活`,
|
||||||
|
> Task 4 加了 `enabled` 这一维后该注释已不完整。改为 `// 组件启用 + 未冷却 + 未运行 + 存活`。
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试**
|
- [ ] **Step 1: 写失败测试**
|
||||||
|
|
||||||
@@ -2838,6 +2863,11 @@ grep -rn "BossSkill\|WeakPoint\|Telegraph\|AttackPattern\|SkillSequence\|StopBeh
|
|||||||
在各自任务里改到)。**本步负责清掉剩余的**:逐个改为只描述功能的表述,不删除功能性内容。
|
在各自任务里改到)。**本步负责清掉剩余的**:逐个改为只描述功能的表述,不删除功能性内容。
|
||||||
清完后再跑一次 grep,此时才应无命中。
|
清完后再跑一次 grep,此时才应无命中。
|
||||||
|
|
||||||
|
**已点名的一处需特别处理**:`EnemyQuotaManager` 有序列化字段 `_maxActiveBehaviorTrees`
|
||||||
|
与注释 `等价于旧的禁用行为树`(Task 4 执行时发现)。字段改名会丢已有场景/预制体上的配置值,
|
||||||
|
必须加 `[UnityEngine.Serialization.FormerlySerializedAs("_maxActiveBehaviorTrees")]`
|
||||||
|
再改为 `_maxActiveBrains`(该管理器实际裁剪的是 `EnemyAiBrain`,不是能力组件)。
|
||||||
|
|
||||||
- [ ] **Step 3: 更新指导手册**
|
- [ ] **Step 3: 更新指导手册**
|
||||||
|
|
||||||
在 `Docs/Guides/` 的敌人 / Boss 章节新增一节「Boss AI 怎么写」,内容覆盖:
|
在 `Docs/Guides/` 的敌人 / Boss 章节新增一节「Boss AI 怎么写」,内容覆盖:
|
||||||
|
|||||||
Reference in New Issue
Block a user