- AiRecipeSO 新增 protected InvalidateGraph(),子类装配模块后调用以丢弃缓存图,
避免改动要等到下次进 Play 才生效的静默陈旧图问题。
- PerceptionRecipeSO.SetModulesForTests 改名为 AssignModules(向导也要用它装配模块,
旧名字具有误导性),并在装配后调用 InvalidateGraph。
- 新增 EnemyAiRecipeWizard:按 AssetFolderSpec 定名定路径创建感知型 AI 配方资产
(Assets/_Game/Data/Enemies/{EnemyID}/ENM_{EnemyID}_Ai.asset),已存在则选中不覆盖。
- PerceptionRecipeSoTests 同步改名调用点,并新增测试钉住"装配后缓存必须失效"契约。
99 lines
3.8 KiB
C#
99 lines
3.8 KiB
C#
using NUnit.Framework;
|
||
using UnityEngine;
|
||
using BaseGames.AI;
|
||
using BaseGames.Enemies;
|
||
|
||
namespace BaseGames.Tests.EditMode.AI
|
||
{
|
||
public class PerceptionRecipeSoTests
|
||
{
|
||
sealed class Ctx : IAiContext, IEnemyActor
|
||
{
|
||
public FakeSensor S = new FakeSensor();
|
||
public FakeLocomotion L = new FakeLocomotion();
|
||
public FakeCombat C = new FakeCombat();
|
||
public FakeVitals V = new FakeVitals();
|
||
public Blackboard BB = new Blackboard();
|
||
public ISensor Sensor => S;
|
||
public IEnemyLocomotion Locomotion => L;
|
||
public ICombatant Combat => C;
|
||
public IActorVitals Vitals => V;
|
||
public Blackboard Blackboard => BB;
|
||
public bool HasAlertState => true;
|
||
}
|
||
|
||
static PerceptionRecipeSO MakeE001Recipe()
|
||
{
|
||
var so = ScriptableObject.CreateInstance<PerceptionRecipeSO>();
|
||
so.AssignModules(
|
||
new DisguiseThenPatrol(),
|
||
new RushEngagement("e001_chase", RushExit.Committed));
|
||
return so;
|
||
}
|
||
|
||
[Test]
|
||
public void Graph_IsCachedAcrossCalls_Flyweight()
|
||
{
|
||
var so = MakeE001Recipe();
|
||
Assert.AreSame(so.GetOrBuildGraph(), so.GetOrBuildGraph());
|
||
Object.DestroyImmediate(so);
|
||
}
|
||
|
||
[Test]
|
||
public void Graph_MatchesEquivalentHandWrittenAssembly()
|
||
{
|
||
var so = MakeE001Recipe();
|
||
var fromRecipe = so.GetOrBuildGraph();
|
||
|
||
var b = new BrainBuilder();
|
||
PerceptionSkeleton.Add(b,
|
||
new DisguiseThenPatrol(),
|
||
new RushEngagement("e001_chase", RushExit.Committed));
|
||
var handWritten = b.Build();
|
||
|
||
Assert.AreEqual(handWritten.EntryState, fromRecipe.EntryState);
|
||
Object.DestroyImmediate(so);
|
||
}
|
||
|
||
[Test]
|
||
public void Graph_BehavesLikeE001()
|
||
{
|
||
var so = MakeE001Recipe();
|
||
var ctx = new Ctx();
|
||
var rt = new AiRuntime(so.GetOrBuildGraph(), ctx);
|
||
Assert.AreEqual(DisguiseThenPatrol.Disguise, rt.CurrentStateName);
|
||
ctx.S.Chase = true;
|
||
rt.Tick(0.1f);
|
||
Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName);
|
||
Object.DestroyImmediate(so);
|
||
}
|
||
|
||
[Test]
|
||
public void UnconfiguredRecipe_ThrowsClearError_NotSilentlyInert()
|
||
{
|
||
// 新建的空配方默认用 RushEngagement 但尚未配能力。建图时必须**显式报错**,
|
||
// 而不是静默产出一张"永远不会出手"的图——第 6 条:暴露问题,不掩盖。
|
||
// 注:策划建完资产到配好模块之间没有任何东西会调 GetOrBuildGraph(),
|
||
// 它只在 EnemyAiBrain.Start() 运行时被调,所以这里报错不影响编辑期体验。
|
||
var so = ScriptableObject.CreateInstance<PerceptionRecipeSO>();
|
||
var ex = Assert.Throws<System.InvalidOperationException>(() => so.GetOrBuildGraph());
|
||
StringAssert.Contains("RushEngagement", ex.Message);
|
||
Object.DestroyImmediate(so);
|
||
}
|
||
|
||
[Test]
|
||
public void AssignModules_InvalidatesCachedGraph()
|
||
{
|
||
// 不失效的话,装配后拿到的还是旧图——静默的陈旧配置。
|
||
var so = MakeE001Recipe();
|
||
var first = so.GetOrBuildGraph();
|
||
so.AssignModules(new SinglePost(LocomotionMode.Idle),
|
||
new RushEngagement("other", RushExit.OnLostTarget));
|
||
var second = so.GetOrBuildGraph();
|
||
Assert.AreNotSame(first, second);
|
||
Assert.AreEqual(SinglePost.Post, second.EntryState); // 新模块生效
|
||
Object.DestroyImmediate(so);
|
||
}
|
||
}
|
||
}
|