From 6df4a259b7c47eef12925dd6b9bbf734b97e09de Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 29 Jul 2026 16:34:21 +0800 Subject: [PATCH] =?UTF-8?q?docs(enemy):=20=E4=BF=AE=E6=AD=A3=E4=BD=9C?= =?UTF-8?q?=E8=80=85=E6=8C=87=E5=8D=97=E4=B8=8E=E4=B8=A4=E5=A4=84=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E6=AE=8B=E7=95=99=E7=9A=84=E6=AD=BB=E4=BA=A1=E5=B1=82?= =?UTF-8?q?=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Tests/EditMode/AI/AiStateFragmentsTests.cs | 2 +- .../_Game/Scripts/Enemies/AIBrain/Modules/IUnawareModule.cs | 2 +- .../Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs | 2 +- Docs/Guides/08_EnemyAi_Authoring_Guide.md | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Assets/Tests/EditMode/AI/AiStateFragmentsTests.cs b/Assets/Tests/EditMode/AI/AiStateFragmentsTests.cs index a5c02489..bfe448a8 100644 --- a/Assets/Tests/EditMode/AI/AiStateFragmentsTests.cs +++ b/Assets/Tests/EditMode/AI/AiStateFragmentsTests.cs @@ -130,7 +130,7 @@ namespace BaseGames.Tests.EditMode.AI [Test] public void Fragments_Throw_WhenDifferentFragmentKindsCollide() { - // 跨层撞名同样要挡:未发现层的 Locomotion 态与死亡层的 Ability 态重名。 + // 跨层撞名同样要挡:未发现层的 Locomotion 态与交战层的 Ability 态重名。 var b = new BrainBuilder(); AiStateFragments.Locomotion(b, "Shared", LocomotionMode.Patrol); Assert.Throws( diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/IUnawareModule.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/IUnawareModule.cs index 5c09184e..41b4cac1 100644 --- a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/IUnawareModule.cs +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/IUnawareModule.cs @@ -18,7 +18,7 @@ namespace BaseGames.Enemies /// 挂内部转换(如站立 ⇄ 巡逻的计时边)。没有内部边的模块留空实现。 void Link(BrainBuilder b); - /// 图入口(出生态)。注意与交战/死亡层的 EntryState 不同: + /// 图入口(出生态)。注意与交战层的 EntryState 不同: /// 本属性会成为整张图唯一的 BrainBuilder.Entry,而 EntryState 只是别处 To() 的目标。 string Entry { get; } diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs index 5ccee8cf..2c0a4f6c 100644 --- a/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs @@ -16,7 +16,7 @@ namespace BaseGames.Enemies [Tooltip("交战层:发现玩家之后怎么打")] [SerializeReference, SubclassSelector] IEngagementModule _engagement = new RushEngagement(); - // 无「死亡层」字段:死亡归物理层,骨架自己声明 Death 终态。 + // 死亡不做成第三层可插拔构件:死亡归物理层,骨架自己声明 Death 终态。 // PerformDeath 先 ForceState(Dead) 再发 Died 信号,此后 IsControllable 永久为假, // 死亡链里的条件边永不被求值——所以死亡不能做成可插拔层。 diff --git a/Docs/Guides/08_EnemyAi_Authoring_Guide.md b/Docs/Guides/08_EnemyAi_Authoring_Guide.md index c1d812eb..94ad3a76 100644 --- a/Docs/Guides/08_EnemyAi_Authoring_Guide.md +++ b/Docs/Guides/08_EnemyAi_Authoring_Guide.md @@ -168,8 +168,9 @@ Build(b): 1. **未发现层内部边必须放 `Link()`,不能放 `Declare()`。** `AiRuntime.TryTransition` 按**声明顺序**遍历当前态的转换列表,条件都满足时先声明的先命中。`PerceptionSkeleton.Add` 的调用顺序固定为 `Declare → 挂升级边 → Link`:升级边(进警觉/进交战)必须先于未发现层的内部边(如巡逻计时)声明。如果把内部边挂进 `Declare()`,它会排在升级边前面——玩家进入追逐区那一帧,若恰好计时器也到点,敌人会切去另一个待机态而不是扑上来。 -2. **死亡态用 `AiStateFragments.AbilityOnce`,不用 `Ability`。** - `Ability()` 建的态每帧 `Tick` 都会 `EnsureAbility`(若能力已结束则重新触发),用于"只要还在这个态就该一直在做"的追击/冲锋类持续态。死亡演出只该播一次——用 `Ability()` 会在演出播完后被下一帧 `Tick` 重新触发,死亡动画无限重播。`AbilityOnce()` 只在 `OnEnter` 触发一次,没有 `Tick`。 +2. **一次性演出用 `AiStateFragments.AbilityOnce`,不用 `Ability`。** + `Ability()` 建的态每帧 `Tick` 都会 `EnsureAbility`(若能力已结束则重新触发),这是为"只要还在这个态就该一直在做"的持续态设计的——比如追击/冲锋类态,中途被受击/硬直打断后,下一帧 `Tick` 会自动把能力重新触发回来,符合"停了就该接着做"的语义。但如果态本身是一次性演出,`Ability()` 就用错了:演出播完后下一帧 `Tick` 仍会把它重新触发一遍,动画无限重播、态永远退不出去。真实的一次性态例子(均来自设计文档,尚未实现)——E004 蛭母出场时的 `Appear`(吼叫示威,播完衔接 `Idle`,见 `Docs/Game/敌人/小怪/E004_蛭母.md`)、E002 簧蛭钻出啃咬的 `Skill_Start`(钻出动作打完转 `Skill_Loop` 悬挂,见 `Docs/Game/敌人/小怪/E002_簧蛭.md`)——都该用 `AbilityOnce()`:只在 `OnEnter` 触发一次,没有 `Tick`,态自己的转换边(如 `.To(...).When(!IsAbilityRunning())`)负责在演出播完后离开。 + 注意:死亡态不适用本条——`Ability`/`AbilityOnce` 都不该用在死亡态上,死亡态的坑是转换边完全不会被求值(见下一条),不是"演出重播"的问题;`Death` 直接用 `Terminal` 声明为无行为终态。 3. **死亡链里不能放条件边,死亡完全归物理层。** `EnemyBase.PerformDeath` 先 `ForceState(EnemyStateType.Dead)`,再 `_brain?.Send(AiSignal.Died)`。`ForceState` 之后 `EnemyBrainContext.IsControllable`(`_enemy.CurrentState == EnemyStateType.Controlled`)永久为假;`AiRuntime.Tick` 里事件转换(`Died` 信号)先于 `IsControllable` 让位门处理,能正常切到 `Death` 终态,但**此后**任何条件边都不会再被求值(`Tick` 在让位门处直接 `return`)。所以骨架把 `Death` 声明成 `AiStateFragments.Terminal`(无行为终态),不做成可插拔层——死亡演出/清理/对象池归还全部交给 `EnemyBase.PerformDeath`、`EnemyDeathSequence`(前摇演出)、`EnemySpawnerOnEvent`(死亡生成小怪)等物理层组件。