【问题标题】:Enumerating Child Classes for Selection枚举子类以供选择
【发布时间】:2022-10-18 16:35:41
【问题描述】:

我正在制作一个编辑器工具,它允许我将部分添加到我制作的可脚本化对象类的实例中。我使用通用方法添加新部分:

[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 语句以提高可读性。
  • 您不需要实例化每个子类并将其添加到列表中!只需将基类的新实例添加到列表中:public void AddPart() { parts.Add(new PartBase()); 稍后您可以将任何子类实例分配到列表中:'parts[n] = AnIstanceOfBazPart; ...`
  • @klekmek 是说没有其他选择不需要我写一个新的“条目”,只要我想添加一个新的部分类型,字典仍然意味着我有枚举并且 switch 语句需要我每当我添加一个新部分时都需要一个新的枚举
  • @Behnam这很容易知道,但是它并没有改变这样一个事实,即在某些时候我需要决定什么类进入任何给定列表槽,此外,我不能按照你的建议做,因为 PartBase 是抽象的
  • 你的目标到底是什么?作为后备,您仍然可以进行反射并通过Activator.CreateInstance 创建实例...无论如何,如果PartBase 实际上是ScriptableObject,正如您所说,那么通过new 创建实例有什么好处@全部?我宁愿希望用户通过资产创建菜单创建这些实例,然后将它们拖到 Inspector 的插槽中......?

标签: c# unity3d parent-child enumeration


【解决方案1】:

我知道这有点超出了你的问题的范围,但你去吧。

代码中的 cmets 应该希望能解释每一步

[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 { }
}

【讨论】:

    猜你喜欢
    • 2019-10-18
    • 1970-01-01
    • 1970-01-01
    • 2022-08-22
    • 1970-01-01
    • 1970-01-01
    • 2021-03-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多