简体   繁体   English

Unity Custom Inspector带有子检查员

[英]Unity Custom Inspector with sub-inspectors

I am working on a small ARPG in Unity 2017.2. 我正在Unity 2017中开发一个小型ARPG。

I have tried implementing a custom editor for the AbilityBluePrint class of my game. 我已经尝试为我的游戏的AbilityBluePrint类实现自定义编辑器。

Basically, the AbilityBluePrints contain all the information necessary to generate the Ability at run time. 基本上,AbilityBluePrints包含在运行时生成Ability所需的所有信息。 Including an array of Effect[] ScritpableObjects which get triggered when the ability is used. 包括一个Effect [] ScritpableObjects数组,当使用该功能时会触发它。

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. 假设我有一个效果类DamagePlusX:作为伤害修饰值的效果。 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. 如果我希望这个效果对两个不同的能力有不同的修饰值,那么我将在我的资产目录中创建它的两个实例,并手动将每个实例分配给相应能力的Effect []数组。 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. 因此我认为我会使用自定义检查器,有点像Unity中的Adventure Tutorial

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. 我们的想法是基本上创建AbilityBluePrint的实例,然后使用自定义检查器能够在Effects []数组中动态实例化效果,并能够直接在AbilityBluePrint检查器中编辑每个效果的属性。

Basically I would like to get somthing a bit like that (apologies for the poor photoshop): 基本上我想得到一些类似的东西(为可怜的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. 在原始教程中,我的BluePrintAbility相当于Monobehaviour。

The code I have is below: 我的代码如下:

My BluePrintAbility Class: 我的BluePrintAbility类:

[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: 我的DamagePlusX效果类:

[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. 不确定它是否会对您的确切情况有所帮助,但我已经幸运地将数据存储在纯C#类中,然后在ScriptableObject中嵌套这些数组,并且两者上的自定义编辑器一起工作。

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: 然后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;
}

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

All of which results in: 所有这些导致:

Example Inspector 示例检查器

Anyway hope that example gives you some ideas that might help with yours. 无论如何希望这个例子能给你一些可能有助于你的想法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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