feat(enemy): PerceptionRecipeSO 配方资产 + SubclassSelector 模块下拉
未发现层/交战层各用 [SerializeReference]+[SubclassSelector] 下拉挑选模块, 新增模块类自动出现在下拉里,无需改枚举或已有文件。默认交战模块仍为 RushEngagement——未配能力时建图必须显式报错,而非静默产出打不出手的图。
This commit is contained in:
@@ -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:
|
||||
Reference in New Issue
Block a user