diff --git a/Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs b/Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs index be6dce39..ddca8932 100644 --- a/Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs +++ b/Assets/Tests/EditMode/AI/PerceptionRecipeSoTests.cs @@ -25,7 +25,7 @@ namespace BaseGames.Tests.EditMode.AI static PerceptionRecipeSO MakeE001Recipe() { var so = ScriptableObject.CreateInstance(); - so.SetModulesForTests( + so.AssignModules( new DisguiseThenPatrol(), new RushEngagement("e001_chase", RushExit.Committed)); return so; @@ -80,5 +80,19 @@ namespace BaseGames.Tests.EditMode.AI 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); + } } } diff --git a/Assets/_Game/Scripts/AI/AiRecipeSO.cs b/Assets/_Game/Scripts/AI/AiRecipeSO.cs index 779274ec..1b8cf38c 100644 --- a/Assets/_Game/Scripts/AI/AiRecipeSO.cs +++ b/Assets/_Game/Scripts/AI/AiRecipeSO.cs @@ -25,6 +25,10 @@ namespace BaseGames.AI void ClearCache() => _cached = null; + /// 装配 / 修改模块后调用,丢弃已缓存的图。 + /// 不调的话,改动要等到下次进入 Play 才生效——静默的陈旧图。 + protected void InvalidateGraph() => _cached = null; + public AiGraph GetOrBuildGraph() { if (_cached == null) diff --git a/Assets/_Game/Scripts/Editor/AI/EnemyAiRecipeWizard.cs b/Assets/_Game/Scripts/Editor/AI/EnemyAiRecipeWizard.cs new file mode 100644 index 00000000..68867bd0 --- /dev/null +++ b/Assets/_Game/Scripts/Editor/AI/EnemyAiRecipeWizard.cs @@ -0,0 +1,74 @@ +using System.IO; +using UnityEditor; +using UnityEngine; +using BaseGames.Enemies; + +namespace BaseGames.Editor.AI +{ + /// + /// 敌人 AI 配方脚手架。按 AssetFolderSpec 定名定路径: + /// Assets/_Game/Data/Enemies/{EnemyID}/ENM_{EnemyID}_Ai.asset + /// + public sealed class EnemyAiRecipeWizard : EditorWindow + { + const string DataRoot = "Assets/_Game/Data/Enemies"; + + string _enemyId = "E001"; + + [MenuItem("BaseGames/AI/Enemy AI Recipe Wizard")] + static void Open() => GetWindow("敌人 AI 配方向导").minSize = new Vector2(420, 160); + + void OnGUI() + { + EditorGUILayout.HelpBox( + "创建感知型 AI 配方资产。创建后在 Inspector 里选两层模块(未发现 / 交战)," + + "再把资产拖到敌人预制体的 EnemyAiBrain._recipe 上。", + MessageType.Info); + + _enemyId = EditorGUILayout.TextField("敌人 ID(如 E001)", _enemyId); + + string path = TargetPath(_enemyId); + EditorGUILayout.LabelField("产出路径", path); + + using (new EditorGUI.DisabledScope(string.IsNullOrWhiteSpace(_enemyId))) + { + if (GUILayout.Button("创建配方资产", GUILayout.Height(28))) + Create(_enemyId); + } + } + + static string TargetPath(string enemyId) => + $"{DataRoot}/{enemyId}/ENM_{enemyId}_Ai.asset"; + + /// 建配方资产。已存在则选中返回,不覆盖用户已配好的内容。 + public static PerceptionRecipeSO Create(string enemyId) + { + string dir = $"{DataRoot}/{enemyId}"; + if (!AssetDatabase.IsValidFolder(dir)) + { + Directory.CreateDirectory(dir); + AssetDatabase.Refresh(); + } + + string path = TargetPath(enemyId); + var existing = AssetDatabase.LoadAssetAtPath(path); + if (existing != null) + { + Selection.activeObject = existing; + EditorGUIUtility.PingObject(existing); + Debug.Log($"配方已存在,已选中:{path}"); + return existing; + } + + var so = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(so, path); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + Selection.activeObject = so; + EditorGUIUtility.PingObject(so); + Debug.Log($"✔ 已创建 AI 配方:{path}"); + return so; + } + } +} diff --git a/Assets/_Game/Scripts/Editor/AI/EnemyAiRecipeWizard.cs.meta b/Assets/_Game/Scripts/Editor/AI/EnemyAiRecipeWizard.cs.meta new file mode 100644 index 00000000..61a0ccb0 --- /dev/null +++ b/Assets/_Game/Scripts/Editor/AI/EnemyAiRecipeWizard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a27970d31831f6747a3995481fde3d39 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs index 1f91684d..5ccee8cf 100644 --- a/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Recipes/PerceptionRecipeSO.cs @@ -23,10 +23,12 @@ namespace BaseGames.Enemies protected override void Build(BrainBuilder b) => PerceptionSkeleton.Add(b, _unaware, _engagement); - /// 仅供 EditMode 测试与脚手架向导装配模块,运行时不使用。 - public void SetModulesForTests(IUnawareModule unaware, IEngagementModule engagement) + /// 装配模块。供脚手架向导与 EditMode 测试使用,运行时不应调用 + /// (本资产是 flyweight,运行时改它会影响所有引用该配方的敌人)。 + public void AssignModules(IUnawareModule unaware, IEngagementModule engagement) { _unaware = unaware; _engagement = engagement; + InvalidateGraph(); } } }