From afcc0b10eec645c76ad6f61022ad5ca6cba69c7e Mon Sep 17 00:00:00 2001 From: Joywayer Date: Thu, 30 Jul 2026 08:33:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(enemy):=20=E9=85=8D=E6=96=B9=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=94=B9=E7=94=A8=E4=B8=80=E6=AC=A1=E6=80=A7=20builde?= =?UTF-8?q?r=20=E6=8E=A2=E6=B5=8B=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=B9=B2?= =?UTF-8?q?=E6=89=B0=E5=85=B1=E4=BA=AB=E5=9B=BE=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs | 11 ++++++----- Assets/_Game/Scripts/AI/AiRecipeSO.cs | 10 ++++++++++ .../Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs | 13 +++++++++---- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs b/Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs index 1e58e3a6..a6649097 100644 --- a/Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs +++ b/Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs @@ -117,14 +117,15 @@ namespace BaseGames.Tests.EditMode.AI } [Test] - public void Validate_DoesNotLeaveCachedGraph() + public void Validate_DoesNotDisturbCachedGraph() { - // 校验期试建的图不能留给运行时——否则改了模块后拿到的是校验时的旧图。 + // 校验用一次性 builder 探测,不得动共享缓存——否则同一配方的下一只敌人 + // 会拿到不同的图实例,破坏 flyweight 不变量。 var so = MakeE001Recipe(); + var before = so.GetOrBuildGraph(); // 先把缓存捂热(不经 AssignModules) Validate(so); - so.AssignModules(new SinglePost(LocomotionMode.Idle), - new RushEngagement("other", RushExit.OnLostTarget)); - Assert.AreEqual(SinglePost.Post, so.GetOrBuildGraph().EntryState); + var after = so.GetOrBuildGraph(); + Assert.AreSame(before, after); // 校验前后必须是同一张图 Object.DestroyImmediate(so); } diff --git a/Assets/_Game/Scripts/AI/AiRecipeSO.cs b/Assets/_Game/Scripts/AI/AiRecipeSO.cs index 1b8cf38c..426017c8 100644 --- a/Assets/_Game/Scripts/AI/AiRecipeSO.cs +++ b/Assets/_Game/Scripts/AI/AiRecipeSO.cs @@ -29,6 +29,16 @@ namespace BaseGames.AI /// 不调的话,改动要等到下次进入 Play 才生效——静默的陈旧图。 protected void InvalidateGraph() => _cached = null; + /// 校验专用:用一次性 BrainBuilder 试建,只为暴露建图期异常。 + /// 刻意不碰 _cached——走 GetOrBuildGraph 会在缓存已热时跳过 Build(等于没探测), + /// 且事后置空缓存会破坏"一个配方共享一张图"的 flyweight 不变量。 + protected void ProbeBuild() + { + var b = new BrainBuilder(); + Build(b); + b.Build(); + } + public AiGraph GetOrBuildGraph() { if (_cached == null) diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs index c8e80b16..002b9ab8 100644 --- a/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs @@ -46,19 +46,24 @@ namespace BaseGames.Enemies yield return ValidationResult.Error($"{who}:未选择交战层模块。"); if (_unaware == null || _engagement == null) yield break; + bool moduleHasError = false; foreach (var r in _engagement.Validate()) + { + if (r.Severity == ValidationSeverity.Error) moduleHasError = true; yield return new ValidationResult(r.Severity, $"{who}:{r.Message}"); + } + if (moduleHasError) yield break; // 模块层已定位到问题,探测只会重复同一条 - // 结构性校验:直接试建一次图,模块与骨架的建图期守卫会把问题抛出来 + // 结构性校验:用一次性 builder 试建,模块与骨架的建图期守卫会把问题抛出来 // (能力漏配、Rest 不在 States、态未声明、跨模块状态名冲突等)。 - // 这是最彻底的检查——凡是运行时会炸的,这里就会炸。 + // 走 ProbeBuild 而非 GetOrBuildGraph:后者在缓存已热时会跳过 Build(等于没探测), + // 且事后置空缓存会破坏 flyweight 不变量。 ValidationResult? buildFailure = null; - try { GetOrBuildGraph(); } + try { ProbeBuild(); } catch (System.Exception e) { buildFailure = ValidationResult.Error($"{who}:建图失败 —— {e.Message}"); } - finally { InvalidateGraph(); } // 校验期建的图不留给运行时 if (buildFailure.HasValue) yield return buildFailure.Value; }