Я делаю инструмент редактора, который позволяет мне добавлять части в созданные мной экземпляры класса объектов сценариев. Я использую общий метод для добавления новых частей:
[CreateAssetMenu]
class Whole : ScriptableObject {
List<PartBase> parts = new();
public void AddPart<T>() where T:PartBase, new() { parts.Add(new T()); }
}
class Foo {
//insert selection statement that goes through each Child class of PartBase
}
То, как я сейчас делаю то, что собираюсь сделать, выглядит так:
switch (EPartType)
{
case EPartType.Bar:
AddPart<BarPart>();
case EPartType.Baz:
AddPart<BazPart>();
case EPartType.Bor:
AddPart<BorPart>();
default:
break;
}
Мне интересно, есть ли способ сделать это, который не требует от меня включения каждого отдельного подкласса PartBase (чтобы пользователи могли добавлять свои собственные пользовательские части, просто создав новый скрипт, наследуемый от PartBase, и не нужно дополнительно возиться с моим безопасным перечислением), в то же время предоставляя какой-то перечисляемый выбор, который можно использовать в качестве раскрывающегося списка в моем инструменте.
Заранее спасибо!
Вам не нужно создавать экземпляр каждого подкласса и добавлять его в список! просто добавьте новый экземпляр базового класса в список: public void AddPart() { parts.Add(new PartBase()); позже вы можете назначить любые экземпляры подкласса в список: 'parts[n] = AnIstanceOfBazPart; ...`
@klekmek означает, что нет альтернативы, которая не требовала бы от меня написания новой «записи» в любое время, когда я хочу добавить новый тип части, словарь по-прежнему означает, что у меня есть перечисление, а оператору switch нужно i нужно новое перечисление каждый раз, когда я добавляю новую часть
@Behnam Это полезно знать, однако это не меняет того факта, что в какой-то момент мне нужно будет решить, какой класс будет входить в любой слот списка предоставления, кроме того, я не могу сделать, как вы советуете, потому что PartBase абстрактен
Какова именно ваша цель? В качестве запасного варианта вы все равно можете подумать и создать свои экземпляры с помощью Activator.CreateInstance ... в любом случае, если PartBase на самом деле является ScriptableObject, как вы говорите, то какой смысл создавать экземпляры через new вообще? Я бы предпочел, чтобы пользователь создавал эти экземпляры через меню создания ресурсов и просто перетаскивал их в слот в Инспекторе...?
@derHugo Моя цель — иметь возможность использовать окно редактора для добавления частей в экземпляры Whole. Это окно представляет собой настраиваемый редактор для Whole экземпляров. Инструмент должен позволять пользователю выбирать тип, производный от PartBase, из раскрывающегося списка, а затем добавлять объект к выбору PartBase в экземпляре Whole. Моя проблема связана с процессом выбора, чтобы решить, какой тип PartBase следует добавить к назначенному Whole. Я ищу альтернативу использованию перечисления и switch, которая не требует модификации (добавление здесь регистра переключения) каждый раз, когда добавляется новая производная PartBase.
@DanielFamakin, зачем вам новый переключатель при использовании словаря? Вы можете передать имеющееся у вас перечисление и получить значение по ключу перечисления из словаря. Это одна операция. Однако заполнение словаря выполняется вручную.
@klekmek я хочу сказать, что я бы предпочел вообще избегать перечисления, так как это инструмент, к которому я хочу, чтобы люди могли добавлять свои собственные части. Ваш способ означает, что где-то есть перечисление и словарь, каждый из которых нуждается в новой записи каждый раз, когда пользователь добавляет часть своего собственного создания. Я бы предпочел сохранить исходный код пакета в чистоте и подальше от рук пользователей, чтобы их работа сводилась к простому созданию классов, наследуемых от моих баз и использующих имеющиеся у меня структуры.
I seek an alternative to using an enum and switch that doesn't require modification (adding a switch case here) every time a new PartBase derivative is added вы можете использовать Reflection и искать все классы, производные от вашего базового класса. Затем вы можете создать свои экземпляры с помощью ScriptableObject.Create(Type) и передать выбранный тип





Я знаю, что это выходит за рамки вашего вопроса, но вот.
Мы надеемся, что комментарии в коде должны объяснять каждый шаг.
[CreateAssetMenu]
public class Whole : ScriptableObject
{
public List<PartBase> parts = new();
#if UNITY_EDITOR
// A custom Inspector for this type to extend it with an additional button
[CustomEditor(typeof(Whole))]
private class WholeEditor : Editor
{
// serialized property for the "parts"
private SerializedProperty m_PartsProperty;
// tiny hack to simulate a dropdown (see below)
private Rect _rect;
private void OnEnable()
{
// link up serialized property
m_PartsProperty = serializedObject.FindProperty(nameof(parts));
}
public override void OnInspectorGUI()
{
// draw the default inspector
base.OnInspectorGUI();
EditorGUILayot.Space();
var buttonClicked = GUILayout.Button("Add Part");
if (Event.current.type == EventType.Repaint)
{
// little hack to get the button rect in order to place
// our popup window here to kinda simulate a dropdown
_rect = GUIUtility.GUIToScreenRect(GUILayoutUtility.GetLastRect());
}
if (buttonClicked)
{
// Get the FOLDER where the current main asset is placed
// we will place created part assets here as well
var mainAssetPath = AssetDatabase.GetAssetPath(target);
var parts = mainAssetPath.Split('/').ToList();
parts.RemoveAt(parts.Count - 1);
mainAssetPath = string.Join('/', parts);
// shift the target position lower to start the window right under the "Add Part" button
_rect.y += _rect.height;
WholeEditorAddPopup.OpenPopup(_rect, m_PartsProperty, mainAssetPath);
}
}
private class WholeEditorAddPopup : EditorWindow
{
// The property where to finally add the created part
private SerializedProperty m_ListProperty;
// available Types and their according display names
private Type[] m_AvailableTypes;
private GUIContent[] m_DisplayOptions;
// just the label for the dropdown
private readonly GUIContent m_Label = new("Part to add");
// Folder where to create assets
private string m_MainAssetPath;
// currently selected type index
private int m_Selected = -1;
public static void OpenPopup(Rect buttonRect, SerializedProperty listProperty, string mainAssetPath)
{
// create a new instance of this window
var window = GetWindow<WholeEditorAddPopup>(true, "Add Part");
// assign the fields
window.m_ListProperty = listProperty;
window.m_MainAssetPath = mainAssetPath;
// get all assemblies
window.m_AvailableTypes = AppDomain.CurrentDomain.GetAssemblies()
// get all Types
.SelectMany(assembly => assembly.GetTypes())
// Filter to only have non-abstract child classes of "PartBase"
.Where(type => type.IsSubclassOf(typeof(PartBase)) && !type.IsAbstract)
// order by "FullName" (=> including namespaces)
.OrderBy(type => type.FullName)
.ToArray();
// For the display names replace all "." by "/"
// => Unity treats those as nested folders in the popup (see demo below)
window.m_DisplayOptions = window.m_AvailableTypes.Select(type => new GUIContent(type.FullName.Replace('.', '/'))).ToArray();
// show as Dropdown -> clicking outside automatically closes window
window.ShowAsDropDown(buttonRect, new Vector2(buttonRect.width, EditorGUIUtility.singleLineHeight * 4));
// [optional] set position again since "ShowAsDropDown" might have hanged it
window.position = new Rect(buttonRect.x, buttonRect.y, buttonRect.width, EditorGUIUtility.singleLineHeight * 4);
}
private void OnGUI()
{
// Draw a dropdown button containing all the available types
// grouped by namespaces and ordered alphabetically
m_Selected = EditorGUILayout.Popup(m_Label, m_Selected, m_DisplayOptions);
// only enable the "Add" button if valid index selected
var blockAdd = m_Selected < 0;
EditorGUILayout.Space();
using (new EditorGUI.DisabledScope(blockAdd))
{
if (GUILayout.Button("Add"))
{
// get selected type by selected index
var selectedType = m_AvailableTypes[m_Selected];
// create runtime ScriptableObject instance by selected type
var part = CreateInstance(selectedType);
// Set its initial name
part.name = $"new {selectedType.Name}";
// Get a unique path for this asset
// => if already an asset with same name Unity adds an auto-incremented index
var path = AssetDatabase.GenerateUniqueAssetPath($"{m_MainAssetPath}/{part.name}.asset");
// Create the asset, save and refresh
AssetDatabase.CreateAsset(part, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// not sure anymore but from an old experience I think you need to re-load the asset
var loadedPart = AssetDatabase.LoadAssetAtPath<PartBase>(path);
// add the loaded asset to the "parts" list
m_ListProperty.arraySize += 1;
var elementProperty = m_ListProperty.GetArrayElementAtIndex(m_ListProperty.arraySize - 1);
elementProperty.objectReferenceValue = loadedPart;
// finally make the modified SerializedProperties persistent in the actual "Whole" asset
m_ListProperty.serializedObject.ApplyModifiedProperties();
// [optional] "Ping" the created asset => get highlighted in the Assets folder
EditorGUIUtility.PingObject(loadedPart);
// Close the popup window
Close();
}
}
}
}
}
#endif
}
А вот небольшая демонстрация того, как это будет выглядеть
Для демонстрации я создал следующие типы — конечно, все в своих отдельных файлах скриптов.
public class PartBase : ScriptableObject { }
public class ExamplePart : PartBase { }
namespace NamespaceA
{
public class PartA : PartBase { }
}
namespace NamespaceA
{
public class PartAExtended : PartA { }
}
namespace NamespaceB
{
public class PartB : PartBase { }
}
namespace NamespaceB
{
public class PartBExtended : PartB { }
}
AppDomain.CurrentDomain.GetAssemblies()... Это ключевая часть, о которой я не знал. Это на высоте, выше и даже выше, спасибо! Я делаю это в отдельном окне, а не в пользовательском инспекторе, но я смогу использовать этот бит, чтобы делать именно то, что мне нужно. Находка.
Вы можете определить словарь перечислений и объектов. Лично мне больше нравятся операторы switch для удобочитаемости.