使用Unity自定义检视面板的子检视面板

3
我正在使用Unity 2017.2制作一款小型动作角色扮演游戏(ARPG)。
我尝试为游戏中的AbilityBluePrint类实现自定义编辑器。
基本上,AbilityBluePrints包含在运行时生成能力所需的所有信息,包括Effect[] ScritpableObjects数组,这些对象在使用能力时触发。
目前,我已经实现并使所有需要的内容工作,但我认为创建能力将会非常乏味,原因如下。
假设我有一个效果类DamagePlusX: Effect,其中包含伤害修饰值。如果我想让该效果的两个不同能力具有不同的修饰值,则必须在我的资产目录中创建两个实例,并手动将每个实例分配给相应能力的Effect[]数组。我担心我最终将拥有大量大量的效果实例,每个实例本质上只有几个不同的整数和浮点数。
因此,我想使用自定义检查器,有点像Unity的 Adventure教程中的检查器。
基本思路是创建AbilityBluePrint实例,然后使用自定义检查器能够动态实例化Effects数组中的效果,并能够直接在AbilityBluePrint检查器中编辑每个效果的属性。
基本上我想要得到类似于这样的东西(对于糟糕的Photoshop表示歉意):

enter image description here

我试图将教程中的脚本转换为符合我的需求,但自昨天以来一直出现相同的错误:

NullReferenceException: Object reference not set to an instance of an object
AbilityBluePrintEditor.SubEditorSetup (.EffectEditor editor) (at Assets/Scripts/Editor/AbilityBluePrintEditor.cs:90)
EditorWithSubEditors`2[TEditor,TTarget].CheckAndCreateSubEditors (.TTarget[] subEditorTargets) (at Assets/Scripts/Editor/EditorWithSubEditors.cs:33)

我尝试了很多东西,想知道我正在尝试的脚本对象是否可行。在原始教程中,我的 BluePrintAbility 相当于一个 Monobehaviour。

我拥有的代码如下:

我的 BluePrintAbility 类:

[CreateAssetMenu(fileName = "New Ability BluePrint", menuName = "Ability BluePrint")]
public class AbilityBluePrint : ScriptableObject {
    public Effect[] effects = new Effect[0];
    public string description;
}

我的Effect类:

public abstract class Effect : ScriptableObject {
    }

我的 DamagePlusX 效果类:

[CreateAssetMenu(fileName = "DamagePlusX",menuName = "Effects/DamagePlusX")]
public class DamagePlusX : Effect
{
    [SerializeField]
    int modifier;

    public void ApplyModifier(){ // some logic}
}

现在是编辑器(抱歉样本很长,但我不知道错误在哪里,尽管我已经缩减了主要类):

这是教程中的基本编辑器,我的错误就来自于此:

// This class acts as a base class for Editors that have Editors
// nested within them.  For example, the InteractableEditor has
// an array of ConditionCollectionEditors.
// It's generic types represent the type of Editor array that are
// nested within this Editor and the target type of those Editors.
public abstract class EditorWithSubEditors<TEditor, TTarget> : Editor
    where TEditor : Editor
    where TTarget : Object

{
    protected TEditor[] subEditors;         // Array of Editors nested within this Editor.


// This should be called in OnEnable and at the start of OnInspectorGUI.
protected void CheckAndCreateSubEditors (TTarget[] subEditorTargets)
{
    // If there are the correct number of subEditors then do nothing.
    if (subEditors != null && subEditors.Length == subEditorTargets.Length)
        return;

    // Otherwise get rid of the editors.
    CleanupEditors ();

    // Create an array of the subEditor type that is the right length for the targets.
    subEditors = new TEditor[subEditorTargets.Length];

    // Populate the array and setup each Editor.
    for (int i = 0; i < subEditors.Length; i++)
    {
        subEditors[i] = CreateEditor (subEditorTargets[i]) as TEditor;
        SubEditorSetup (subEditors[i]); // ERROR comes inside this function HERE !!!!
    }
}


// This should be called in OnDisable.
protected void CleanupEditors ()
{
    // If there are no subEditors do nothing.
    if (subEditors == null)
        return;

    // Otherwise destroy all the subEditors.
    for (int i = 0; i < subEditors.Length; i++)
    {
        DestroyImmediate (subEditors[i]);
    }

    // Null the array so it's GCed.
    subEditors = null;
}


// This must be overridden to provide any setup the subEditor needs when it is first created.
protected abstract void SubEditorSetup (TEditor editor);

}

    [CustomEditor(typeof(AbilityBluePrint)), CanEditMultipleObjects]
public class AbilityBluePrintEditor : EditorWithSubEditors<EffectEditor, Effect>
{
    private AbilityBluePrint blueprint;          // Reference to the target.
    private SerializedProperty effectsProperty; //represents the array of effects.

    private Type[] effectTypes;                           // All the non-abstract types which inherit from Effect.  This is used for adding new Effects.
    private string[] effectTypeNames;                     // The names of all appropriate Effect types.
    private int selectedIndex;                              // The index of the currently selected Effect type.


    private const float dropAreaHeight = 50f;               // Height in pixels of the area for dropping scripts.
    private const float controlSpacing = 5f;                // Width in pixels between the popup type selection and drop area.
    private const string effectsPropName = "effects";   // Name of the field for the array of Effects.


    private readonly float verticalSpacing = EditorGUIUtility.standardVerticalSpacing;
    // Caching the vertical spacing between GUI elements.

    private void OnEnable()
    {
        // Cache the target.
        blueprint = (AbilityBluePrint)target;

        // Cache the SerializedProperty
        effectsProperty = serializedObject.FindProperty(effectsPropName);

        // If new editors for Effects are required, create them.
        CheckAndCreateSubEditors(blueprint.effects);

        // Set the array of types and type names of subtypes of Reaction.
        SetEffectNamesArray();
    }

    public override void OnInspectorGUI()
    {
        // Pull all the information from the target into the serializedObject.
        serializedObject.Update();

        // If new editors for Reactions are required, create them.
        CheckAndCreateSubEditors(blueprint.effects);

        DrawDefaultInspector();

        // Display all the Effects.
        for (int i = 0; i < subEditors.Length; i++)
        {
            if (subEditors[i] != null)
            {
                subEditors[i].OnInspectorGUI();
            }            
        }

        // If there are Effects, add a space.
        if (blueprint.effects.Length > 0)
        {
            EditorGUILayout.Space();
            EditorGUILayout.Space();
        }


        //Shows the effect selection GUI
        SelectionGUI();

        if (GUILayout.Button("Add Effect"))
        {

        }

        // Push data back from the serializedObject to the target.
        serializedObject.ApplyModifiedProperties();
    }

    private void OnDisable()
    {
        // Destroy all the subeditors.
        CleanupEditors();
    }

    // This is called immediately after each ReactionEditor is created.
    protected override void SubEditorSetup(EffectEditor editor)
    {
        // Make sure the ReactionEditors have a reference to the array that contains their targets.
        editor.effectsProperty = effectsProperty; //ERROR IS HERE !!!
    }

    private void SetEffectNamesArray()
    {
        // Store the Effect type.
        Type effectType = typeof(Effect);

        // Get all the types that are in the same Assembly (all the runtime scripts) as the Effect type.
        Type[] allTypes = effectType.Assembly.GetTypes();

        // Create an empty list to store all the types that are subtypes of Effect.
        List<Type> effectSubTypeList = new List<Type>();

        // Go through all the types in the Assembly...
        for (int i = 0; i < allTypes.Length; i++)
        {
            // ... and if they are a non-abstract subclass of Effect then add them to the list.
            if (allTypes[i].IsSubclassOf(effectType) && !allTypes[i].IsAbstract)
            {
                effectSubTypeList.Add(allTypes[i]);
            }
        }

        // Convert the list to an array and store it.
        effectTypes = effectSubTypeList.ToArray();

        // Create an empty list of strings to store the names of the Effect types.
        List<string> reactionTypeNameList = new List<string>();

        // Go through all the Effect types and add their names to the list.
        for (int i = 0; i < effectTypes.Length; i++)
        {
            reactionTypeNameList.Add(effectTypes[i].Name);
        }

        // Convert the list to an array and store it.
        effectTypeNames = reactionTypeNameList.ToArray();
    }

    private void SelectionGUI()
    {
        // Create a Rect for the full width of the inspector with enough height for the drop area.
        Rect fullWidthRect = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(dropAreaHeight + verticalSpacing));

        // Create a Rect for the left GUI controls.
        Rect leftAreaRect = fullWidthRect;

        // It should be in half a space from the top.
        leftAreaRect.y += verticalSpacing * 0.5f;

        // The width should be slightly less than half the width of the inspector.
        leftAreaRect.width *= 0.5f;
        leftAreaRect.width -= controlSpacing * 0.5f;

        // The height should be the same as the drop area.
        leftAreaRect.height = dropAreaHeight;

        // Create a Rect for the right GUI controls that is the same as the left Rect except...
        Rect rightAreaRect = leftAreaRect;

        // ... it should be on the right.
        rightAreaRect.x += rightAreaRect.width + controlSpacing;

        // Display the GUI for the type popup and button on the left.
        TypeSelectionGUI(leftAreaRect);
    }

    private void TypeSelectionGUI(Rect containingRect)
    {
        // Create Rects for the top and bottom half.
        Rect topHalf = containingRect;
        topHalf.height *= 0.5f;
        Rect bottomHalf = topHalf;
        bottomHalf.y += bottomHalf.height;

        // Display a popup in the top half showing all the reaction types.
        selectedIndex = EditorGUI.Popup(topHalf, selectedIndex, effectTypeNames);

        // Display a button in the bottom half that if clicked...
        if (GUI.Button(bottomHalf, "Add Selected Effect"))
        {
            // ... finds the type selected by the popup, creates an appropriate reaction and adds it to the array.
            Debug.Log(effectTypes[selectedIndex]);
            Type effectType = effectTypes[selectedIndex];
            Effect newEffect = EffectEditor.CreateEffect(effectType);
            Debug.Log(newEffect);
            effectsProperty.AddToObjectArray(newEffect);
        }
    }
}

public abstract class EffectEditor : Editor
{
    public bool showEffect = true;                       // Is the effect editor expanded?
    public SerializedProperty effectsProperty;    // Represents the SerializedProperty of the array the target belongs to.


    private Effect effect;                      // The target Reaction.


    private const float buttonWidth = 30f;          // Width in pixels of the button to remove this Reaction from the ReactionCollection array.

    private void OnEnable()
    {
        // Cache the target reference.
        effect = (Effect)target;

        // Call an initialisation method for inheriting classes.
        Init();
    }

    // This function should be overridden by inheriting classes that need initialisation.
    protected virtual void Init() { }


    public override void OnInspectorGUI()
    {
        Debug.Log("attempt to draw effect inspector");
        // Pull data from the target into the serializedObject.
        serializedObject.Update();

        EditorGUILayout.BeginVertical(GUI.skin.box);
        EditorGUI.indentLevel++;

        DrawDefaultInspector();

        EditorGUI.indentLevel--;
        EditorGUILayout.EndVertical();

        // Push data back from the serializedObject to the target.
        serializedObject.ApplyModifiedProperties();
    }

    public static Effect CreateEffect(Type effectType)
    {
        // Create a reaction of a given type.
        return (Effect) ScriptableObject.CreateInstance(effectType);
    }
}

[CustomEditor(typeof(DamagePlusXEditor))]
public class DamagePlusXEditor : EffectEditor {}
1个回答

5

不确定这是否适用于您的特定情况,但我曾尝试将数据存储在一个纯C#类中,然后在ScriptableObject中嵌套一个该类的数组,并且这两个自定义编辑器可以配合使用。

例如,这是一个纯数据类(它还由其他相当简单的纯类组成):

[System.Serializable]
public class Buff
{
    public CharacterAttribute attribute;
    public CalculationType calculationType;
    public BuffDuration buffDuration;
    public bool effectBool;
    public int effectInt;
    public float effectFloat;
}

使用类似以下编辑器的编辑器:
[CustomPropertyDrawer (typeof (Buff))]
public class BuffDrawer : PropertyDrawer
{
    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    ...

接着,SO包含了一个由这些“Buff”对象组成的数组:

[CreateAssetMenu (fileName = "New Buff", menuName = "Data/Buff")]
public class BuffData : ScriptableObject
{
    public new string name;
    public string description;
    public Texture2D icon;
    public Buff [] attributeBuffs;
}

最后是SO的编辑器(请看底部的PropertyField):
using UnityEngine;
using UnityEditor;

[CustomEditor (typeof (BuffData))]
public class BuffDataEditor : Editor
{
    private const int DescriptionWidthPadding = 35;
    private const float DescriptionHeightPadding = 1.25f;
    private const string AttributesHelpText = 
        "Choose which attributes are to be affected by this buff and by how much.\n" +
        "Note: the calculation type should match the attribute's implementation.";

    private SerializedProperty nameProperty;
    private SerializedProperty descriptionProperty;
    private SerializedProperty iconProperty;
    private SerializedProperty attributeBuffsProperty;

    private void OnEnable ()
    {
        nameProperty = serializedObject.FindProperty ("name");
        descriptionProperty = serializedObject.FindProperty ("description");
        iconProperty = serializedObject.FindProperty ("icon");
        attributeBuffsProperty = serializedObject.FindProperty ("attributeBuffs");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update ();

        nameProperty.stringValue = EditorGUILayout.TextField ("Name", nameProperty.stringValue);
        EditorGUILayout.LabelField ("Description:");
        GUIStyle descriptionStyle = new GUIStyle (EditorStyles.textArea)
        {
            wordWrap = true,
            padding = new RectOffset (6, 6, 6, 6),
            fixedWidth = Screen.width - DescriptionWidthPadding
        };
        descriptionStyle.fixedHeight = descriptionStyle.CalcHeight (new GUIContent (descriptionProperty.stringValue), Screen.width) * DescriptionHeightPadding;
        EditorGUI.indentLevel++;
        descriptionProperty.stringValue = EditorGUILayout.TextArea (descriptionProperty.stringValue, descriptionStyle);
        EditorGUI.indentLevel--;
        EditorGUILayout.Space ();
        iconProperty.objectReferenceValue = (Texture2D) EditorGUILayout.ObjectField ("Icon", iconProperty.objectReferenceValue, typeof (Texture2D), false);
        EditorGUILayout.Space ();
        EditorGUILayout.HelpBox (AttributesHelpText, MessageType.Info);
        EditorGUILayout.PropertyField (attributeBuffsProperty, true);

        serializedObject.ApplyModifiedProperties();
    }
}

所有这些都导致: 示例检查器 希望这个示例会给你一些想法,以帮助你自己的项目。

谢谢您的建议。我会考虑一下使用您的方法需要做哪些改变才能让它正常工作。如果不需要进行重大更改,我将接受这个答案。 - Sorade

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接