diff --git a/Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs b/Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs new file mode 100644 index 00000000..b9e40fb5 --- /dev/null +++ b/Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs @@ -0,0 +1,48 @@ +using NUnit.Framework; +using BaseGames.AI; + +namespace BaseGames.Tests.EditMode.AI +{ + // 顶层测试定义,供反射注册表发现 + [AiDefinition("__test_grunt")] + internal sealed class TestGruntAi : AiScript + { + protected override void Build(BrainBuilder b) + { + b.Entry("A"); + b.State("A").To("B").When(c => c.Sensor.SeesPlayer(), "SeesPlayer"); + b.State("B"); + } + } + + public class AiDefinitionRegistryTests + { + [Test] + public void Has_FindsRegisteredDefinition() + { + Assert.IsTrue(AiDefinitionRegistry.Has("__test_grunt")); + } + + [Test] + public void GetGraph_ResolvesById() + { + var g = AiDefinitionRegistry.GetGraph("__test_grunt"); + Assert.AreEqual("A", g.EntryState); + } + + [Test] + public void GetGraph_SameId_ReturnsSharedInstance() + { + Assert.AreSame( + AiDefinitionRegistry.GetGraph("__test_grunt"), + AiDefinitionRegistry.GetGraph("__test_grunt")); + } + + [Test] + public void GetGraph_UnknownId_Throws() + { + Assert.Throws( + () => AiDefinitionRegistry.GetGraph("__nope__")); + } + } +} diff --git a/Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs.meta b/Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs.meta new file mode 100644 index 00000000..1aa02327 --- /dev/null +++ b/Assets/Tests/EditMode/AI/AiDefinitionRegistryTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 830591c077a45b54f9f56e6debdb035b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs b/Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs new file mode 100644 index 00000000..ddf4a9b0 --- /dev/null +++ b/Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace BaseGames.AI +{ + /// + /// 反射收集所有 [AiDefinition] 的 AiScript 子类,按 id 提供共享 AiGraph(flyweight)。 + /// 同一 id 全实例共享同一 AiScript 实例 → 同一张不可变 AiGraph。 + /// + public static class AiDefinitionRegistry + { + static Dictionary _byId; + + // 项目已关闭 Domain Reload:静态缓存需在进入播放时重置,保证按最新类型重建。 + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void ResetOnPlay() => _byId = null; + + static void EnsureBuilt() + { + if (_byId != null) return; + _byId = new Dictionary(); + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + Type[] types; + try { types = asm.GetTypes(); } + catch (ReflectionTypeLoadException e) { types = e.Types; } + if (types == null) continue; + for (int i = 0; i < types.Length; i++) + { + var t = types[i]; + if (t == null || t.IsAbstract || !typeof(AiScript).IsAssignableFrom(t)) continue; + var attr = t.GetCustomAttribute(); + if (attr == null) continue; + if (_byId.ContainsKey(attr.Id)) + throw new InvalidOperationException( + $"AiDefinitionRegistry: 重复的 AI 定义 id '{attr.Id}'({t.FullName})。"); + _byId[attr.Id] = (AiScript)Activator.CreateInstance(t); + } + } + } + + public static bool Has(string id) + { + EnsureBuilt(); + return _byId.ContainsKey(id); + } + + /// 取 id 对应的共享 AiGraph;不存在则显式抛错(根因暴露,不做兜底)。 + public static AiGraph GetGraph(string id) + { + EnsureBuilt(); + if (!_byId.TryGetValue(id, out var script)) + throw new InvalidOperationException( + $"AiDefinitionRegistry: 未找到 AI 定义 id '{id}'。请确认存在 [AiDefinition(\"{id}\")] 的 AiScript 子类。"); + return script.GetOrBuildGraph(); + } + + public static IEnumerable Ids + { + get { EnsureBuilt(); return _byId.Keys; } + } + } +} diff --git a/Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs.meta b/Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs.meta new file mode 100644 index 00000000..32cb47d6 --- /dev/null +++ b/Assets/_Game/Scripts/AI/AiDefinitionRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3becea437ed3fcf45b936e3a26054adf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: