feat(editor): 敌人 AI 配方脚手架向导;AssignModules 改名并失效缓存
- AiRecipeSO 新增 protected InvalidateGraph(),子类装配模块后调用以丢弃缓存图,
避免改动要等到下次进 Play 才生效的静默陈旧图问题。
- PerceptionRecipeSO.SetModulesForTests 改名为 AssignModules(向导也要用它装配模块,
旧名字具有误导性),并在装配后调用 InvalidateGraph。
- 新增 EnemyAiRecipeWizard:按 AssetFolderSpec 定名定路径创建感知型 AI 配方资产
(Assets/_Game/Data/Enemies/{EnemyID}/ENM_{EnemyID}_Ai.asset),已存在则选中不覆盖。
- PerceptionRecipeSoTests 同步改名调用点,并新增测试钉住"装配后缓存必须失效"契约。
This commit is contained in:
@@ -25,7 +25,7 @@ namespace BaseGames.Tests.EditMode.AI
|
|||||||
static PerceptionRecipeSO MakeE001Recipe()
|
static PerceptionRecipeSO MakeE001Recipe()
|
||||||
{
|
{
|
||||||
var so = ScriptableObject.CreateInstance<PerceptionRecipeSO>();
|
var so = ScriptableObject.CreateInstance<PerceptionRecipeSO>();
|
||||||
so.SetModulesForTests(
|
so.AssignModules(
|
||||||
new DisguiseThenPatrol(),
|
new DisguiseThenPatrol(),
|
||||||
new RushEngagement("e001_chase", RushExit.Committed));
|
new RushEngagement("e001_chase", RushExit.Committed));
|
||||||
return so;
|
return so;
|
||||||
@@ -80,5 +80,19 @@ namespace BaseGames.Tests.EditMode.AI
|
|||||||
StringAssert.Contains("RushEngagement", ex.Message);
|
StringAssert.Contains("RushEngagement", ex.Message);
|
||||||
Object.DestroyImmediate(so);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ namespace BaseGames.AI
|
|||||||
|
|
||||||
void ClearCache() => _cached = null;
|
void ClearCache() => _cached = null;
|
||||||
|
|
||||||
|
/// <summary>装配 / 修改模块后调用,丢弃已缓存的图。
|
||||||
|
/// 不调的话,改动要等到下次进入 Play 才生效——静默的陈旧图。</summary>
|
||||||
|
protected void InvalidateGraph() => _cached = null;
|
||||||
|
|
||||||
public AiGraph GetOrBuildGraph()
|
public AiGraph GetOrBuildGraph()
|
||||||
{
|
{
|
||||||
if (_cached == null)
|
if (_cached == null)
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.IO;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using BaseGames.Enemies;
|
||||||
|
|
||||||
|
namespace BaseGames.Editor.AI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 敌人 AI 配方脚手架。按 AssetFolderSpec 定名定路径:
|
||||||
|
/// Assets/_Game/Data/Enemies/{EnemyID}/ENM_{EnemyID}_Ai.asset
|
||||||
|
/// </summary>
|
||||||
|
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<EnemyAiRecipeWizard>("敌人 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";
|
||||||
|
|
||||||
|
/// <summary>建配方资产。已存在则选中返回,不覆盖用户已配好的内容。</summary>
|
||||||
|
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<PerceptionRecipeSO>(path);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
Selection.activeObject = existing;
|
||||||
|
EditorGUIUtility.PingObject(existing);
|
||||||
|
Debug.Log($"配方已存在,已选中:{path}");
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var so = ScriptableObject.CreateInstance<PerceptionRecipeSO>();
|
||||||
|
AssetDatabase.CreateAsset(so, path);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
|
||||||
|
Selection.activeObject = so;
|
||||||
|
EditorGUIUtility.PingObject(so);
|
||||||
|
Debug.Log($"✔ 已创建 AI 配方:{path}");
|
||||||
|
return so;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a27970d31831f6747a3995481fde3d39
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -23,10 +23,12 @@ namespace BaseGames.Enemies
|
|||||||
protected override void Build(BrainBuilder b)
|
protected override void Build(BrainBuilder b)
|
||||||
=> PerceptionSkeleton.Add(b, _unaware, _engagement);
|
=> PerceptionSkeleton.Add(b, _unaware, _engagement);
|
||||||
|
|
||||||
/// <summary>仅供 EditMode 测试与脚手架向导装配模块,运行时不使用。</summary>
|
/// <summary>装配模块。供脚手架向导与 EditMode 测试使用,运行时不应调用
|
||||||
public void SetModulesForTests(IUnawareModule unaware, IEngagementModule engagement)
|
/// (本资产是 flyweight,运行时改它会影响所有引用该配方的敌人)。</summary>
|
||||||
|
public void AssignModules(IUnawareModule unaware, IEngagementModule engagement)
|
||||||
{
|
{
|
||||||
_unaware = unaware; _engagement = engagement;
|
_unaware = unaware; _engagement = engagement;
|
||||||
|
InvalidateGraph();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user