From fd238b71f1f9803626cae5189632ba732771b24b Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 29 Jul 2026 11:14:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ai):=20AiSignal=208=E2=86=921=EF=BC=8C?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=9B=B6=E5=8F=91=E9=80=81=E6=96=B9=E9=9B=B6?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=E6=96=B9=E7=9A=84=E6=AD=BB=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Tests/EditMode/AI/AiFrameworkTests.cs | 29 +++++++++++--------- Assets/Tests/EditMode/AI/AiRuntimeTests.cs | 4 +-- Assets/_Game/Scripts/AI/AiSignal.cs | 19 +++++++------ 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Assets/Tests/EditMode/AI/AiFrameworkTests.cs b/Assets/Tests/EditMode/AI/AiFrameworkTests.cs index 5e3cd237..1c52d160 100644 --- a/Assets/Tests/EditMode/AI/AiFrameworkTests.cs +++ b/Assets/Tests/EditMode/AI/AiFrameworkTests.cs @@ -107,10 +107,10 @@ namespace BaseGames.Tests.EditMode.AI b.Entry("A"); b.State("A") .To("ByCond").When(c => true, "cond") // 条件恒真 - .To("ByEvent").OnEvent(AiSignal.Parried); // 事件优先(Tick 第1步) + .To("ByEvent").OnEvent(AiSignal.Died); // 事件优先(Tick 第1步) b.State("ByCond"); b.State("ByEvent"); var rt = new AiRuntime(b.Build(), new FakeAiContext()); - rt.Send(AiSignal.Parried); + rt.Send(AiSignal.Died); rt.Tick(0.1f); Assert.AreEqual("ByEvent", rt.CurrentStateName); } @@ -125,27 +125,30 @@ namespace BaseGames.Tests.EditMode.AI var ctx = new FakeAiContext(); ctx.S.Sees = true; var rt = new AiRuntime(b.Build(), ctx); - rt.Send(AiSignal.Parried); // A 无 Parried 转换 → 被消费掉,不阻塞条件评估 + rt.Send(AiSignal.Died); // A 无 Died 转换 → 被消费掉,不阻塞条件评估 rt.Tick(0.1f); Assert.AreEqual("B", rt.CurrentStateName); } [Test] - public void MultipleQueuedSignals_FirstMatchWins_RemainingConsumed() + public void QueuedDuplicateSignals_TransitionOnce_RemainderConsumed() { + // 注:原测试用两个不同信号验证"第一个匹配的先转"。AiSignal 现只有 Died 一个值, + // 该形态暂不可表达;等将来出现第二个真实信号(如战斗激活)时恢复此覆盖。 + // 此处覆盖仍然成立的部分:残留信号被消费掉,不会重复触发转换。 var b = new BrainBuilder(); b.Entry("A"); - b.State("A") - .To("B").OnEvent(AiSignal.Parried) - .To("C").OnEvent(AiSignal.Staggered); - b.State("B"); b.State("C"); - var rt = new AiRuntime(b.Build(), new FakeAiContext()); - rt.Send(AiSignal.Parried); - rt.Send(AiSignal.Staggered); + b.State("A").To("B").OnEvent(AiSignal.Died); + b.State("B").OnEnter(c => c.Blackboard.Set("enters", c.Blackboard.Get("enters") + 1)); + var ctx = new FakeAiContext(); + var rt = new AiRuntime(b.Build(), ctx); + rt.Send(AiSignal.Died); + rt.Send(AiSignal.Died); rt.Tick(0.1f); - Assert.AreEqual("B", rt.CurrentStateName); // 第一个匹配的 Parried 先转 B + Assert.AreEqual("B", rt.CurrentStateName); rt.Tick(0.1f); - Assert.AreEqual("B", rt.CurrentStateName); // 残留 Staggered 在 B 无匹配 → 被消费,仍 B + Assert.AreEqual("B", rt.CurrentStateName); + Assert.AreEqual(1, ctx.BB.Get("enters")); // 残留信号被消费,未重入 B } // ── 自转换 / OnTick 时序 ────────────────────────────────────────── diff --git a/Assets/Tests/EditMode/AI/AiRuntimeTests.cs b/Assets/Tests/EditMode/AI/AiRuntimeTests.cs index 1842699c..ab5559f5 100644 --- a/Assets/Tests/EditMode/AI/AiRuntimeTests.cs +++ b/Assets/Tests/EditMode/AI/AiRuntimeTests.cs @@ -165,11 +165,11 @@ namespace BaseGames.Tests.EditMode.AI { var b = new BrainBuilder(); b.Entry("Idle"); - b.State("Idle").To("Flee").OnEvent(AiSignal.Parried); + b.State("Idle").To("Flee").OnEvent(AiSignal.Died); b.State("Flee"); var ctx = new FakeAiContext(); var rt = new AiRuntime(b.Build(), ctx); - rt.Send(AiSignal.Parried); + rt.Send(AiSignal.Died); rt.Tick(0.1f); Assert.AreEqual("Flee", rt.CurrentStateName); } diff --git a/Assets/_Game/Scripts/AI/AiSignal.cs b/Assets/_Game/Scripts/AI/AiSignal.cs index b681bb6e..c7994bfc 100644 --- a/Assets/_Game/Scripts/AI/AiSignal.cs +++ b/Assets/_Game/Scripts/AI/AiSignal.cs @@ -1,15 +1,16 @@ namespace BaseGames.AI { - /// 决策层的推送信号,用于事件驱动的状态转换(不靠轮询)。 + /// + /// 决策层的推送信号,用于事件驱动的状态转换(不靠轮询)。 + /// + /// 只保留真实存在发送方与消费方的信号。受击 / 硬直 / 击飞 / 弹反**不在此列**—— + /// 它们由敌人物理状态机(EnemyStateType)处理,决策层经 IActorVitals.IsControllable + /// 让位门自动挂起。在这里重复定义会诱导写出与物理层打架的第二套受击逻辑。 + /// + /// 新增信号的判据:已经有确定的发送方与消费方,否则不要预留。 + /// public enum AiSignal { - Damaged, // 受到伤害 - Parried, // 被弹反 - Staggered, // 进入硬直 - KnockedUp, // 被击飞 - Died, // 死亡 - PlayerSpotted,// 首次侦测到玩家 - AbilityEnded, // 当前能力协程结束/被打断 - PhaseChanged // Boss 阶段变更 + Died, } }