Перечисление дочерних классов для выбора

Я делаю инструмент редактора, который позволяет мне добавлять части в созданные мной экземпляры класса объектов сценариев. Я использую общий метод для добавления новых частей:

[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, и не нужно дополнительно возиться с моим безопасным перечислением), в то же время предоставляя какой-то перечисляемый выбор, который можно использовать в качестве раскрывающегося списка в моем инструменте.

Заранее спасибо!

Вы можете определить словарь перечислений и объектов. Лично мне больше нравятся операторы switch для удобочитаемости.

klekmek 13.10.2022 16:11

Вам не нужно создавать экземпляр каждого подкласса и добавлять его в список! просто добавьте новый экземпляр базового класса в список: public void AddPart() { parts.Add(new PartBase()); позже вы можете назначить любые экземпляры подкласса в список: 'parts[n] = AnIstanceOfBazPart; ...`

Behnam 13.10.2022 16:40

@klekmek означает, что нет альтернативы, которая не требовала бы от меня написания новой «записи» в любое время, когда я хочу добавить новый тип части, словарь по-прежнему означает, что у меня есть перечисление, а оператору switch нужно i нужно новое перечисление каждый раз, когда я добавляю новую часть

Daniel Famakin 13.10.2022 16:48

@Behnam Это полезно знать, однако это не меняет того факта, что в какой-то момент мне нужно будет решить, какой класс будет входить в любой слот списка предоставления, кроме того, я не могу сделать, как вы советуете, потому что PartBase абстрактен

Daniel Famakin 13.10.2022 16:50

Какова именно ваша цель? В качестве запасного варианта вы все равно можете подумать и создать свои экземпляры с помощью Activator.CreateInstance ... в любом случае, если PartBase на самом деле является ScriptableObject, как вы говорите, то какой смысл создавать экземпляры через new вообще? Я бы предпочел, чтобы пользователь создавал эти экземпляры через меню создания ресурсов и просто перетаскивал их в слот в Инспекторе...?

derHugo 13.10.2022 16:56

@derHugo Моя цель — иметь возможность использовать окно редактора для добавления частей в экземпляры Whole. Это окно представляет собой настраиваемый редактор для Whole экземпляров. Инструмент должен позволять пользователю выбирать тип, производный от PartBase, из раскрывающегося списка, а затем добавлять объект к выбору PartBase в экземпляре Whole. Моя проблема связана с процессом выбора, чтобы решить, какой тип PartBase следует добавить к назначенному Whole. Я ищу альтернативу использованию перечисления и switch, которая не требует модификации (добавление здесь регистра переключения) каждый раз, когда добавляется новая производная PartBase.

Daniel Famakin 13.10.2022 17:43

@DanielFamakin, зачем вам новый переключатель при использовании словаря? Вы можете передать имеющееся у вас перечисление и получить значение по ключу перечисления из словаря. Это одна операция. Однако заполнение словаря выполняется вручную.

klekmek 14.10.2022 09:41

@klekmek я хочу сказать, что я бы предпочел вообще избегать перечисления, так как это инструмент, к которому я хочу, чтобы люди могли добавлять свои собственные части. Ваш способ означает, что где-то есть перечисление и словарь, каждый из которых нуждается в новой записи каждый раз, когда пользователь добавляет часть своего собственного создания. Я бы предпочел сохранить исходный код пакета в чистоте и подальше от рук пользователей, чтобы их работа сводилась к простому созданию классов, наследуемых от моих баз и использующих имеющиеся у меня структуры.

Daniel Famakin 17.10.2022 03:11
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) и передать выбранный тип
derHugo 17.10.2022 16:10
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
9
78
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я знаю, что это выходит за рамки вашего вопроса, но вот.

Мы надеемся, что комментарии в коде должны объяснять каждый шаг.

[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()... Это ключевая часть, о которой я не знал. Это на высоте, выше и даже выше, спасибо! Я делаю это в отдельном окне, а не в пользовательском инспекторе, но я смогу использовать этот бит, чтобы делать именно то, что мне нужно. Находка.
Daniel Famakin 18.10.2022 20:29

Другие вопросы по теме