diff --git a/Assets/_Game/Scripts/Editor/AI/EnemyAiBrainEditor.cs b/Assets/_Game/Scripts/Editor/AI/EnemyAiBrainEditor.cs new file mode 100644 index 00000000..c102018d --- /dev/null +++ b/Assets/_Game/Scripts/Editor/AI/EnemyAiBrainEditor.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using BaseGames.AI; +using BaseGames.Enemies; + +namespace BaseGames.Editor.AI +{ + /// + /// EnemyAiBrain 的检视器:把「配方资产 / AiScript 定义 id」二选一的不变量提前到编辑期, + /// 并把定义 id 从裸字符串换成下拉(取自 AiDefinitionRegistry)。 + /// 不做任何自动修正——配错就明确报出来,由作者选一条路(CLAUDE.md 第 6 条)。 + /// + [CustomEditor(typeof(EnemyAiBrain))] + [CanEditMultipleObjects] + public sealed class EnemyAiBrainEditor : UnityEditor.Editor + { + const string NoneLabel = "(不用此路径)"; + + SerializedProperty _recipe; + SerializedProperty _definitionId; + + void OnEnable() + { + _recipe = serializedObject.FindProperty("_recipe"); + _definitionId = serializedObject.FindProperty("_definitionId"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUILayout.PropertyField(_recipe); + DrawDefinitionIdDropdown(); + + DrawInvariantHelp(); + + // 其余字段(若将来新增)照常绘制 + DrawPropertiesExcluding(serializedObject, "m_Script", "_recipe", "_definitionId"); + + serializedObject.ApplyModifiedProperties(); + } + + void DrawDefinitionIdDropdown() + { + var ids = new List { NoneLabel }; + ids.AddRange(AiDefinitionRegistry.Ids.OrderBy(x => x)); + + string current = _definitionId.stringValue; + int index = string.IsNullOrEmpty(current) ? 0 : ids.IndexOf(current); + + // 已配的 id 在注册表里找不到(脚本被删/改名)——保留原值并显式标出,不静默清空 + bool missing = index < 0; + if (missing) + { + ids.Add($"{current} ⚠ 注册表中不存在"); + index = ids.Count - 1; + } + + using (new EditorGUI.DisabledScope(_recipe.objectReferenceValue != null)) + { + int picked = EditorGUILayout.Popup( + new GUIContent("定制脚本 id", "[AiDefinition(id)] 的 AiScript 子类 id。与配方资产二选一"), + index, ids.Select(s => new GUIContent(s)).ToArray()); + + if (picked != index) + _definitionId.stringValue = picked == 0 ? string.Empty : ids[picked]; + } + + if (missing) + EditorGUILayout.HelpBox( + $"定义 id '{current}' 在 AiDefinitionRegistry 中不存在。" + + "对应的 [AiDefinition] AiScript 子类可能已被删除或改名。", MessageType.Error); + } + + void DrawInvariantHelp() + { + if (_recipe.hasMultipleDifferentValues || _definitionId.hasMultipleDifferentValues) + return; // 多选且不一致时不下断言 + + bool hasRecipe = _recipe.objectReferenceValue != null; + bool hasId = !string.IsNullOrEmpty(_definitionId.stringValue); + + if (hasRecipe && hasId) + EditorGUILayout.HelpBox( + "配方资产与定制脚本 id 同时配置了。二者互斥,运行时 Awake 会报错并禁用本组件。" + + "请清掉其中一项。", MessageType.Error); + else if (!hasRecipe && !hasId) + EditorGUILayout.HelpBox( + "尚未配置 AI 来源。约 95% 的敌人应指定配方资产(用菜单 " + + "BaseGames/AI/Enemy AI Recipe Wizard 创建);有独门机制的敌人才写 " + + "[AiDefinition] 的 AiScript 子类并在此选它的 id。", MessageType.Error); + } + } +} diff --git a/Assets/_Game/Scripts/Editor/AI/EnemyAiBrainEditor.cs.meta b/Assets/_Game/Scripts/Editor/AI/EnemyAiBrainEditor.cs.meta new file mode 100644 index 00000000..f8982c89 --- /dev/null +++ b/Assets/_Game/Scripts/Editor/AI/EnemyAiBrainEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1916dc03357f54d48a974ff1bbd377ba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: