feat(enemy): PerceptionRecipeSO 配方资产 + SubclassSelector 模块下拉

未发现层/交战层各用 [SerializeReference]+[SubclassSelector] 下拉挑选模块,
新增模块类自动出现在下拉里,无需改枚举或已有文件。默认交战模块仍为
RushEngagement——未配能力时建图必须显式报错,而非静默产出打不出手的图。
This commit is contained in:
2026-07-29 14:46:41 +08:00
parent 04f644969b
commit 825cae7977
10 changed files with 298 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dae1fd2dea427e043b89785c3f824370
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using BaseGames.Enemies;
namespace BaseGames.Editor.AI
{
/// <summary>
/// [SerializeReference] + [SubclassSelector] 字段的下拉绘制器:
/// 反射收集该接口的所有非抽象 [Serializable] 实现,选中即替换实例。
/// </summary>
[CustomPropertyDrawer(typeof(SubclassSelectorAttribute))]
public sealed class SubclassSelectorDrawer : PropertyDrawer
{
static readonly Dictionary<Type, Type[]> _cache = new Dictionary<Type, Type[]>();
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.ManagedReference)
{
EditorGUI.PropertyField(position, property, label, true);
return;
}
var baseType = GetManagedReferenceFieldType(property);
var options = GetImplementations(baseType);
var line = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
string currentName = ShortName(property.managedReferenceFullTypename);
int currentIndex = Array.FindIndex(options, t => t.Name == currentName);
var labels = options.Select(t => new GUIContent(t.Name)).ToArray();
EditorGUI.LabelField(line, label);
int picked = EditorGUI.Popup(
new Rect(line.x + EditorGUIUtility.labelWidth + 2f, line.y,
line.width - EditorGUIUtility.labelWidth - 2f, line.height),
currentIndex, labels);
if (picked >= 0 && picked != currentIndex)
{
property.managedReferenceValue = Activator.CreateInstance(options[picked]);
property.serializedObject.ApplyModifiedProperties();
}
EditorGUI.indentLevel++;
float y = position.y + EditorGUIUtility.singleLineHeight + 2f;
foreach (var child in Children(property))
{
float h = EditorGUI.GetPropertyHeight(child, true);
EditorGUI.PropertyField(new Rect(position.x, y, position.width, h), child, true);
y += h + 2f;
}
EditorGUI.indentLevel--;
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float h = EditorGUIUtility.singleLineHeight + 2f;
if (property.propertyType == SerializedPropertyType.ManagedReference)
foreach (var child in Children(property))
h += EditorGUI.GetPropertyHeight(child, true) + 2f;
return h;
}
static IEnumerable<SerializedProperty> Children(SerializedProperty property)
{
var it = property.Copy();
var end = it.GetEndProperty();
if (!it.NextVisible(true)) yield break;
while (!SerializedProperty.EqualContents(it, end))
{
yield return it.Copy();
if (!it.NextVisible(false)) break;
}
}
static string ShortName(string managedReferenceFullTypename)
{
if (string.IsNullOrEmpty(managedReferenceFullTypename)) return null;
int dot = managedReferenceFullTypename.LastIndexOf('.');
return dot >= 0 ? managedReferenceFullTypename.Substring(dot + 1)
: managedReferenceFullTypename.Split(' ').Last();
}
static Type GetManagedReferenceFieldType(SerializedProperty property)
{
// 形如 "Assembly TypeFullName"
string full = property.managedReferenceFieldTypename;
var parts = full.Split(' ');
if (parts.Length != 2) return typeof(object);
return AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == parts[0])?.GetType(parts[1]) ?? typeof(object);
}
static Type[] GetImplementations(Type baseType)
{
if (_cache.TryGetValue(baseType, out var cached)) return cached;
var found = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty<Type>(); } })
.Where(t => t != null && !t.IsAbstract && !t.IsInterface
&& baseType.IsAssignableFrom(t)
&& t.IsDefined(typeof(SerializableAttribute), false)
&& t.GetConstructor(Type.EmptyTypes) != null)
.OrderBy(t => t.Name)
.ToArray();
_cache[baseType] = found;
return found;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee0f164717b32fb4d8e37bd6ca4d1370
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: