feat(enemy): AI 配方接入 SO 校验——漏配/歧义/零冷却 Committed 在编辑期与构建期报错
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.AI;
|
||||
using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
@@ -40,5 +42,9 @@ namespace BaseGames.Enemies
|
||||
.OnExit (x => x.Combat.InterruptAbilities())
|
||||
.To(Approach).When(x => !x.Combat.IsAbilityRunning(), "attackDone");
|
||||
}
|
||||
|
||||
// 本模块无自身配置:招式的射程/冷却/权重全归敌人身上的 EnemyAttackSelector,
|
||||
// 而那是 GameObject 上的组件,配方资产看不到,无法在此校验。
|
||||
public IEnumerable<ValidationResult> Validate() => Array.Empty<ValidationResult>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.AI;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Enemies.Abilities;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
@@ -52,6 +54,27 @@ namespace BaseGames.Enemies
|
||||
s.To(rest).When(AiStateFragments.LostAllZones, "leftAllZones");
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate()
|
||||
{
|
||||
if (_ability.IsEmpty)
|
||||
{
|
||||
yield return ValidationResult.Error("RushEngagement:未配置冲锋能力。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (_ability.HasConflict)
|
||||
yield return ValidationResult.Warning(
|
||||
"RushEngagement:能力引用同时配了资产和字符串 id,资产优先、字符串被忽略。");
|
||||
|
||||
// Committed 的防抖动保证依赖能力自身有冷却:cooldown==0 时能力结束的下一帧
|
||||
// CanUseAbility 即恢复为真,冲锋态↔未发现态每帧抖动照旧出现。
|
||||
if (_exit == RushExit.Committed && _ability.Asset != null && _ability.Asset.cooldown <= 0f)
|
||||
yield return ValidationResult.Error(
|
||||
$"RushEngagement:脱战语义为 Committed,但所引用能力 '{_ability.Asset.name}' 的 cooldown 为 " +
|
||||
$"{_ability.Asset.cooldown}。Committed 的防抖动保证依赖 cooldown > 0,否则冲锋打完下一帧" +
|
||||
"就会再次触发,出现每帧抖动。请给该能力配置冷却,或把脱战语义改为 OnLostTarget。");
|
||||
}
|
||||
|
||||
// 校验放这里而非构造函数:反序列化路径不走 ctor,漏配必须在建图时暴露。
|
||||
string RequireAbilityId()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.AI;
|
||||
using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
@@ -31,5 +33,9 @@ namespace BaseGames.Enemies
|
||||
|
||||
/// <summary>CanEngage 的可读标签(trace / 图导出用)。</summary>
|
||||
string EngageLabel { get; }
|
||||
|
||||
/// <summary>校验本模块自身的配置。由 PerceptionRecipeSO.Validate() 汇总上报。
|
||||
/// 无可失效配置的模块返回空序列。</summary>
|
||||
IEnumerable<ValidationResult> Validate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.AI;
|
||||
using BaseGames.Core;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
@@ -8,7 +10,7 @@ namespace BaseGames.Enemies
|
||||
/// 两个下拉各选一个模块即可,无需写代码。有独门机制的敌人改写 AiScript 子类。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/AI/感知型 AI 配方", fileName = "ENM_")]
|
||||
public sealed class PerceptionRecipeSO : AiRecipeSO
|
||||
public sealed class PerceptionRecipeSO : AiRecipeSO, IValidatable
|
||||
{
|
||||
[Tooltip("未发现层:玩家尚未被发现时的行为形状")]
|
||||
[SerializeReference, SubclassSelector] IUnawareModule _unaware = new SinglePost();
|
||||
@@ -30,5 +32,35 @@ namespace BaseGames.Enemies
|
||||
_unaware = unaware; _engagement = engagement;
|
||||
InvalidateGraph();
|
||||
}
|
||||
|
||||
/// <summary>SOValidationRunner 自动扫描调用。结构性校验(模块是否选、建图是否成功)
|
||||
/// 由配方负责;模块自身的配置(能力引用、冷却语义等)委托给模块的 Validate()——
|
||||
/// 新增一种打法不需要回来改这里。</summary>
|
||||
public IEnumerable<ValidationResult> Validate()
|
||||
{
|
||||
string who = name;
|
||||
|
||||
if (_unaware == null)
|
||||
yield return ValidationResult.Error($"{who}:未选择未发现层模块。");
|
||||
if (_engagement == null)
|
||||
yield return ValidationResult.Error($"{who}:未选择交战层模块。");
|
||||
if (_unaware == null || _engagement == null) yield break;
|
||||
|
||||
foreach (var r in _engagement.Validate())
|
||||
yield return new ValidationResult(r.Severity, $"{who}:{r.Message}");
|
||||
|
||||
// 结构性校验:直接试建一次图,模块与骨架的建图期守卫会把问题抛出来
|
||||
// (能力漏配、Rest 不在 States、态未声明、跨模块状态名冲突等)。
|
||||
// 这是最彻底的检查——凡是运行时会炸的,这里就会炸。
|
||||
ValidationResult? buildFailure = null;
|
||||
try { GetOrBuildGraph(); }
|
||||
catch (System.Exception e)
|
||||
{
|
||||
buildFailure = ValidationResult.Error($"{who}:建图失败 —— {e.Message}");
|
||||
}
|
||||
finally { InvalidateGraph(); } // 校验期建的图不留给运行时
|
||||
|
||||
if (buildFailure.HasValue) yield return buildFailure.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace BaseGames.Enemies.Abilities
|
||||
|
||||
[NonSerialized] string _literal; // 定制脚本路径用;不参与序列化
|
||||
|
||||
/// <summary>引用的能力资产;走字符串路径时为 null。供校验器读取冷却等配置。</summary>
|
||||
public EnemyAbilitySO Asset => _asset;
|
||||
|
||||
public AbilityRef(EnemyAbilitySO asset) { _asset = asset; _literal = null; }
|
||||
public AbilityRef(string id) { _asset = null; _literal = id; }
|
||||
public AbilityRef(EnemyAbilitySO asset, string id) { _asset = asset; _literal = id; }
|
||||
|
||||
Reference in New Issue
Block a user