简体   繁体   中英

Unity Custom Inspector with sub-inspectors

I am working on a small ARPG in Unity 2017.2.

I have tried implementing a custom editor for the AbilityBluePrint class of my game.

Basically, the AbilityBluePrints contain all the information necessary to generate the Ability at run time. Including an array of Effect[] ScritpableObjects which get triggered when the ability is used.

I currently have everything I need implemented and working but I envisage creating abilities to be very tedious for the following reason.

Say I have an effect class DamagePlusX : Effect which as a damage modifier value. If I want this effect to have a different modifier value for two different abilities then I will have to create two instances of it in my Asset directory and manually assign each one to the Effect[] array of the corresponding ability. I am concerned that I will end up having lots and lots of instances of effects each with essentially a few different ints and floats.

I therefore thought I would use a custom inspector a bit like the one from the Adventure Tutorial from Unity.

The idea is to basically create the instance of the AbilityBluePrint and then use the custom inspector to be able to dynamically instantiate Effects in the Effects[] array and be able to edit the properties of each effect directly within the AbilityBluePrint inspector.

Basically I would like to get somthing a bit like that (apologies for the poor photoshop):

在此输入图像描述

I tried to convert the scripts from the tutorial to fit my needs but I keep having the same error since yesterday:

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)

I've tried so many things I am wondering if what I am trying to do is doable with scriptable objects. In the original tutorial the equivalent of my BluePrintAbility is a Monobehaviour.

The code I have is below:

My BluePrintAbility Class:

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

My Effect class:

public abstract class Effect : ScriptableObject {
    }

My DamagePlusX effect Class:

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

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

And now the Editors (apologies for the long samples, but I don't now where the error is in there, I've cut down the main classes though):

This is the base editor from the tutorial, where my error comes from:

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

Not sure if it will help in your exact situation, but I've had some luck with storing data in a pure C# class and then nesting an array of those inside a ScriptableObject, and the custom editors on both of those worked together.

Eg this pure data class (which is also made up of other fairly simple pure classes):

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

with an editor along the lines of:

[CustomPropertyDrawer (typeof (Buff))]
public class BuffDrawer : PropertyDrawer
{
    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    ...

and then the SO containing an array of these "Buff" objects:

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

and finally the SO's editor (see near the bottom for 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();
    }
}

All of which results in:

Example Inspector

Anyway hope that example gives you some ideas that might help with yours.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM