Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

[Editor Tool] Better ScriptableObject Inspector-Editing

Discussion in 'Assets and Asset Store' started by TheVastBernie, Jul 21, 2017.

  1. TheVastBernie

    TheVastBernie

    Joined:
    Mar 25, 2014
    Posts:
    10
    Hi, guys,
    so, I think a lot of you have been using ScriptableObjects with Unity. I myself have been using them not only for things like items or other types of asset databases, but also for ways to store gameplay relevant stats that can easily be edited by designers and makes rapid iteration even easier. One thing that crops up a lot in our production are scriptables with a few parameters that define members like "playerHealth", "playerDamage" or things like that. Jumping from the player inspector to the scriptableObject, changing the property, jumping back to the player, maybe accessing another scriptable with movement data, changing this object, testing the changes, jumping back to the various scriptables, making alterations, iterating, and so on. It can get quite tiring, altering multiple scriptables at once.

    One thing that would help a lot, would be to display the scriptables member variables within the components inspector gui. And since we found no easy accessible solution for that, we made one ourselves.

    Our two simple editor scripts turn what would previously be found in two inspectors

    (Player Object)


    (Scriptable Object)



    Into an easy to manage and edit, single foldout inspector:

    (Player and Scriptable Object combined)


    If any of you want to try it out or offer feedback, please check out the code below.

    Simply place those two scripts inside the editor folder:

    ScriptableObjectDrawer.cs
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using UnityEditor;
    4.  
    5. [CustomPropertyDrawer(typeof(ScriptableObject), true)]
    6. public class ScriptableObjectDrawer : PropertyDrawer
    7. {
    8.     // Cached scriptable object editor
    9.     private Editor editor = null;
    10.  
    11.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    12.     {
    13.         // Draw label
    14.         EditorGUI.PropertyField(position, property, label, true);
    15.      
    16.         // Draw foldout arrow
    17.         if (property.objectReferenceValue != null)
    18.         {
    19.             property.isExpanded = EditorGUI.Foldout(position, property.isExpanded, GUIContent.none);
    20.         }
    21.  
    22.         // Draw foldout properties
    23.         if (property.isExpanded)
    24.         {
    25.             // Make child fields be indented
    26.             EditorGUI.indentLevel++;
    27.          
    28.             // Draw object properties
    29.             if (!editor)
    30.                 Editor.CreateCachedEditor(property.objectReferenceValue, null, ref editor);
    31.             editor.OnInspectorGUI();
    32.          
    33.             // Set indent back to what it was
    34.             EditorGUI.indentLevel--;
    35.         }
    36.     }
    37. }
    MonoBehaviourEditor.cs
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. [CanEditMultipleObjects]
    5. [CustomEditor(typeof(MonoBehaviour), true)]
    6. public class MonoBehaviourEditor : Editor
    7. {
    8. }
    This one is mandatory since without it, the custom property drawer will throw errors. You need a custom editor class of the component utilising a ScriptableObject. So we just create a dummy editor, that can be used for every MonoBehaviour. With this empty implementation it doesn't alter anything, it just removes Unitys property drawing bug.

    Please try it out, I'm open for feedback and suggestions. Other than that, I hope this was useful and enjoy!

    Edit: For easier inporting, here is a link to a unitypackage
    https://www.dropbox.com/s/rccxlbjgrys77c9/ScriptableObjectEditorFoldout.unitypackage
     
    Last edited: Jul 31, 2017
  2. Lecht

    Lecht

    Joined:
    Jul 1, 2014
    Posts:
    24
    Nice work. Only issue I'm running into, is when I'm editing a ScriptableObject array inside of another ScriptableObject. The elements seem to overlap. For instance if I have 2 elements and expand the first element, it shows the second element fields as well.
     
    laurentlavigne likes this.
  3. TheVastBernie

    TheVastBernie

    Joined:
    Mar 25, 2014
    Posts:
    10
    Yeah, this seems to be an quirk of the unity array property drawer. It always happens when editing array elements. I have not found a way around that. However, if anyone figures something out, I'd be happy to add it to the script.

    In other news, I've made a small unitypackage to make import easier. It's under the main post
     
  4. TheVastBernie

    TheVastBernie

    Joined:
    Mar 25, 2014
    Posts:
    10
    Thanks for the advice, isExpanded seems to work quite well. And it removes the dictionary which I didn't like in the first place :)

    The array issue however cannot be solved by simply expanding the property height. The function changes the height of the actual property, meaning the ScriptableObject field and not the additional child fields that are drawn below. I basically draw my 'default' property and add the properties of the ScriptableObject below it. Arrays and their elements however seem to be drawn as one single property. Thus, all array elements are drawn first and the additional properties of the ScriptableObject are then drawn below. Below the array in this case and not below the actual array element.

    Not sure if you can overcome this limitation, but in most cases it seems to work fine ;)
     
  5. Frednaar

    Frednaar

    Joined:
    Apr 18, 2010
    Posts:
    153
    Great tool. If you want to edit SO inside SO you should create an additional script, save it as ScriptableObjectEditor.cs

    under Editor and copy paste the following code


    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using UnityEditor;
    4.  
    5. [CustomPropertyDrawer(typeof(ScriptableObject), true)]
    6. public class ScriptableObjectDrawer : PropertyDrawer
    7. {
    8.     // Static foldout dictionary
    9.     private static Dictionary<System.Type, bool> foldoutByType = new Dictionary<System.Type, bool>();
    10.  
    11.     // Cached scriptable object editor
    12.     private Editor editor = null;
    13.  
    14.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    15.     {
    16.         // Draw label
    17.         EditorGUI.PropertyField(position, property, label, true);
    18.        
    19.         // Draw foldout arrow
    20.         bool foldout = false;
    21.         if (property.objectReferenceValue != null)
    22.         {
    23.             // Store foldout values in a dictionary per object type
    24.             bool foldoutExists = foldoutByType.TryGetValue(property.objectReferenceValue.GetType(), out foldout);
    25.             foldout = EditorGUI.Foldout(position, foldout, GUIContent.none);
    26.             if (foldoutExists)
    27.                 foldoutByType[property.objectReferenceValue.GetType()] = foldout;
    28.             else
    29.                 foldoutByType.Add(property.objectReferenceValue.GetType(), foldout);
    30.         }
    31.  
    32.         // Draw foldout properties
    33.         if (foldout)
    34.         {
    35.             // Make child fields be indented
    36.             EditorGUI.indentLevel++;
    37.            
    38.             // Draw object properties
    39.             if (!editor)
    40.                 Editor.CreateCachedEditor(property.objectReferenceValue, null, ref editor);
    41.             editor.OnInspectorGUI();
    42.            
    43.             // Set indent back to what it was
    44.             EditorGUI.indentLevel--;
    45.         }
    46.     }
    47. }
     
    Eristen likes this.
  6. teknic

    teknic

    Joined:
    Oct 18, 2012
    Posts:
    32
    This is very useful. However, I'm getting errors with KeyCode enums. Anyone know how to fix?
     
  7. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    Very helpful and it works perfectly within a component. However SO generate errors when unfolding nested SO references:
    Code (CSharp):
    1. ArgumentException: Getting control 4's position in a group with only 4 controls when doing Repaint
    I tried casing the OnInspectorGUI with an even type test but that old trick doesn't work in this case. What's the fix?

    Here is the animated version of what happens:
    https://i.imgur.com/vMESulH.gifv
     
    Last edited: Dec 22, 2017
  8. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    Also the background could be darker to improve readability but OnInspectorGUI doesn't return a rect so I don't know what to feed GUI.Box(rect, null) so how do I do that?
    And unfolding nested SO in an array could display below the element instead of at the end of the array, I've tried a few things to do that but again, unsuccesful, any idea?

    Here is a mockup of what I want to do:

    eyizIJB.png
     
    Last edited: Dec 22, 2017
  9. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    fix a bug: when the property that contains the SO is empty it would spit out errors

    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(ScriptableObject), true)]
    2. public class ScriptableObjectDrawer : PropertyDrawer
    3. {
    4.     // Cached scriptable object editor
    5.     private Editor editor = null;
    6.  
    7.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    8.     {
    9.         // Draw label
    10.         EditorGUI.PropertyField(position, property, label, true);
    11.  
    12.         // Draw foldout arrow
    13.         if (property.objectReferenceValue != null)
    14.         {
    15.             property.isExpanded = EditorGUI.Foldout(position, property.isExpanded, GUIContent.none);
    16.         }
    17.  
    18.         // Draw foldout properties
    19.         if (property.isExpanded)
    20.         {
    21.             // Make child fields be indented
    22.             EditorGUI.indentLevel++;
    23.  
    24.             if (!editor)
    25.                 Editor.CreateCachedEditor(property.objectReferenceValue, null, ref editor);
    26.  
    27.             // Draw object properties
    28.             if (editor) // catches empty property
    29.                 editor.OnInspectorGUI();
    30.    
    31.             // Set indent back to what it was
    32.             EditorGUI.indentLevel--;
    33.         }
    34.     }
    35. }
     
  10. villevli

    villevli

    Joined:
    Jan 19, 2016
    Posts:
    87
    I haven't tried this, but the problem might be fixed by replacing the "dummy editor" MonoBehaviourEditor.cs with:

    UnityObjectEditor.cs
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. [CanEditMultipleObjects]
    5. [CustomEditor(typeof(UnityEngine.Object), true)]
    6. public class UnityObjectEditor : Editor
    7. {
    8. }
    This editor should fix the error in both MonoBehaviour and ScriptableObject
     
  11. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    You genius, it works.
    Now the problem I'm seeing is when you change a SO from withing a mono, that SO doesn't register as dirty by collab, maybe it's another collab bug but I think it lacks some apply thingy somewhere.
    I know that there is somewhere in the humongous editor api, a way to auto detect change and mark dirty. Let's see if I can fix that...

    Here we go.

    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(ScriptableObject), true)]
    2. public class ScriptableObjectDrawer : PropertyDrawer
    3. {
    4.     // Cached scriptable object editor
    5.     private Editor editor = null;
    6.  
    7.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    8.     {
    9.         // Draw label
    10.         EditorGUI.PropertyField(position, property, label, true);
    11.  
    12.         // Draw foldout arrow
    13.         if (property.objectReferenceValue != null)
    14.         {
    15.             property.isExpanded = EditorGUI.Foldout(position, property.isExpanded, GUIContent.none);
    16.         }
    17.  
    18.         // Draw foldout properties
    19.         if (property.isExpanded)
    20.         {
    21.             // Make child fields be indented
    22.             EditorGUI.indentLevel++;
    23.  
    24.             if (!editor)
    25.                 Editor.CreateCachedEditor(property.objectReferenceValue, null, ref editor);
    26.  
    27.             // Draw object properties
    28.             EditorGUI.BeginChangeCheck();
    29.             if (editor) // catch for empty property
    30.                 editor.OnInspectorGUI();
    31.             if (EditorGUI.EndChangeCheck())
    32.                 property.serializedObject.ApplyModifiedProperties();
    33.    
    34.             // Set indent back to what it was
    35.             EditorGUI.indentLevel--;
    36.         }
    37.     }
    38. }
     
    Eristen likes this.
  12. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    So after being reached out too, I decided to create a fleshed out and optimized solution for this.

    27f20e19e686a06bf952f68de473c06a.png

    Draws in Lists and Arrays

    Optimized, doesn't use GUILayout

    Customizable Spacing on the Inside and Outside of the border

    Allows Nesting (but not nesting of parent objects, causes stack overflow)


    Thanks to @laurentlavigne I had an idea to put backgrounds in the property drawer that layer on top of each other to clearly show children. This works brilliantly and I created 3 themes that you can change easily using an enum, HelpBox, Darken, Lighten and None.

    90715bed61b26a0e671960a4a9b884c2.png

    90b45ff46da58ec2ca427a2054646d13.png

    486bbdd2fb6b183982a8de3df18f2dd5.png

    To use it, put the ExpandableAttribute on a field that you want to be expandable:

    Code (CSharp):
    1.  
    2.     [ExpandableAttribute]
    3.     public ScriptableObject child;
    4.  
    or:

    Code (CSharp):
    1.  
    2.     [Expandable]
    3.     public ScriptableObject child;
    4.  
    And it will now appear as an expandable field. This also works on MonoBehaviour references, as you can see below I am referencing the Main Camera and I am getting all of the camera fields below.

    MonoBehaviourTest.png

    The code for this solution is shown below. I will also be uploading this to the Asset Store at some point so Redistribute Privately so that I can fix the bugs. The Asset Store will allow me to publish updates to fix "self-nesting" issues, as there are currently issues with StackOverflowExceptions.

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. #if UNITY_EDITOR
    5. using System.Collections.Generic;
    6. using UnityEditor;
    7. #endif
    8.  
    9. /// <summary>
    10. /// Use this property on a ScriptableObject type to allow the editors drawing the field to draw an expandable
    11. /// area that allows for changing the values on the object without having to change editor.
    12. /// </summary>
    13. public class ExpandableAttribute : PropertyAttribute
    14. {
    15.     public ExpandableAttribute ()
    16.     {
    17.  
    18.     }
    19. }
    20.  
    21. #if UNITY_EDITOR
    22. /// <summary>
    23. /// Draws the property field for any field marked with ExpandableAttribute.
    24. /// </summary>
    25. [CustomPropertyDrawer (typeof (ExpandableAttribute), true)]
    26. public class ExpandableAttributeDrawer : PropertyDrawer
    27. {
    28.     // Use the following area to change the style of the expandable ScriptableObject drawers;
    29.     #region Style Setup
    30.     private enum BackgroundStyles
    31.     {
    32.         None,
    33.         HelpBox,
    34.         Darken,
    35.         Lighten
    36.     }
    37.  
    38.     /// <summary>
    39.     /// Whether the default editor Script field should be shown.
    40.     /// </summary>
    41.     private static bool SHOW_SCRIPT_FIELD = false;
    42.  
    43.     /// <summary>
    44.     /// The spacing on the inside of the background rect.
    45.     /// </summary>
    46.     private static float INNER_SPACING = 6.0f;
    47.  
    48.     /// <summary>
    49.     /// The spacing on the outside of the background rect.
    50.     /// </summary>
    51.     private static float OUTER_SPACING = 4.0f;
    52.  
    53.     /// <summary>
    54.     /// The style the background uses.
    55.     /// </summary>
    56.     private static BackgroundStyles BACKGROUND_STYLE = BackgroundStyles.HelpBox;
    57.  
    58.     /// <summary>
    59.     /// The colour that is used to darken the background.
    60.     /// </summary>
    61.     private static Color DARKEN_COLOUR = new Color (0.0f, 0.0f, 0.0f, 0.2f);
    62.  
    63.     /// <summary>
    64.     /// The colour that is used to lighten the background.
    65.     /// </summary>
    66.     private static Color LIGHTEN_COLOUR = new Color (1.0f, 1.0f, 1.0f, 0.2f);
    67.     #endregion
    68.  
    69.     /// <summary>
    70.     /// Cached editor reference.
    71.     /// </summary>
    72.     private Editor editor = null;
    73.  
    74.     public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    75.     {
    76.         float totalHeight = 0.0f;
    77.  
    78.         totalHeight += EditorGUIUtility.singleLineHeight;
    79.  
    80.         if (property.objectReferenceValue == null)
    81.             return totalHeight;
    82.  
    83.         if (!property.isExpanded)
    84.             return totalHeight;
    85.  
    86.         if (editor == null)
    87.             Editor.CreateCachedEditor (property.objectReferenceValue, null, ref editor);
    88.  
    89.         if (editor == null)
    90.             return totalHeight;
    91.        
    92.         SerializedProperty field = editor.serializedObject.GetIterator ();
    93.  
    94.         field.NextVisible (true);
    95.  
    96.         if (SHOW_SCRIPT_FIELD)
    97.         {
    98.             totalHeight += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    99.         }
    100.  
    101.         while (field.NextVisible (true))
    102.         {
    103.             totalHeight += EditorGUI.GetPropertyHeight (field, true) + EditorGUIUtility.standardVerticalSpacing;
    104.         }
    105.  
    106.         totalHeight += INNER_SPACING * 2;
    107.         totalHeight += OUTER_SPACING * 2;
    108.  
    109.         return totalHeight;
    110.     }
    111.  
    112.     public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    113.     {
    114.         Rect fieldRect = new Rect (position);
    115.         fieldRect.height = EditorGUIUtility.singleLineHeight;
    116.  
    117.         EditorGUI.PropertyField (fieldRect, property, label, true);
    118.  
    119.         if (property.objectReferenceValue == null)
    120.         {
    121.             Debug.Log ("It's secretly null");
    122.             return;
    123.         }
    124.        
    125.         property.isExpanded = EditorGUI.Foldout (fieldRect, property.isExpanded, GUIContent.none, true);
    126.  
    127.         if (!property.isExpanded)
    128.             return;
    129.  
    130.         if (editor == null)
    131.             Editor.CreateCachedEditor (property.objectReferenceValue, null, ref editor);
    132.  
    133.         if (editor == null)
    134.         {
    135.             Debug.Log ("Couldn't fetch editor");
    136.             return;
    137.         }
    138.        
    139.        
    140.         #region Format Field Rects
    141.         List<Rect> propertyRects = new List<Rect> ();
    142.         Rect marchingRect = new Rect (fieldRect);
    143.  
    144.         Rect bodyRect = new Rect (fieldRect);
    145.         bodyRect.xMin += EditorGUI.indentLevel * 18;
    146.         bodyRect.yMin += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing
    147.             + OUTER_SPACING;
    148.        
    149.         SerializedProperty field = editor.serializedObject.GetIterator ();
    150.         field.NextVisible (true);
    151.  
    152.         marchingRect.y += INNER_SPACING + OUTER_SPACING;
    153.  
    154.         if (SHOW_SCRIPT_FIELD)
    155.         {
    156.             propertyRects.Add (marchingRect);
    157.             marchingRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    158.         }
    159.  
    160.         while (field.NextVisible (true))
    161.         {
    162.             marchingRect.y += marchingRect.height + EditorGUIUtility.standardVerticalSpacing;
    163.             marchingRect.height = EditorGUI.GetPropertyHeight (field, true);
    164.             propertyRects.Add (marchingRect);
    165.         }
    166.  
    167.         marchingRect.y += INNER_SPACING;
    168.  
    169.         bodyRect.yMax = marchingRect.yMax;
    170.         #endregion
    171.  
    172.         DrawBackground (bodyRect);
    173.  
    174.         #region Draw Fields
    175.         EditorGUI.indentLevel++;
    176.  
    177.         int index = 0;
    178.         field = editor.serializedObject.GetIterator ();
    179.         field.NextVisible (true);
    180.  
    181.         if (SHOW_SCRIPT_FIELD)
    182.         {
    183.             //Show the disabled script field
    184.             EditorGUI.BeginDisabledGroup (true);
    185.             EditorGUI.PropertyField (propertyRects[index], field, true);
    186.             EditorGUI.EndDisabledGroup ();
    187.             index++;
    188.         }
    189.  
    190.         //Replacement for "editor.OnInspectorGUI ();" so we have more control on how we draw the editor
    191.         while (field.NextVisible (true))
    192.         {
    193.             try
    194.             {
    195.                 EditorGUI.PropertyField (propertyRects[index], field, true);
    196.             }
    197.             catch (StackOverflowException)
    198.             {
    199.                 field.objectReferenceValue = null;
    200.                 Debug.LogError ("Detected self-nesting cauisng a StackOverflowException, avoid using the same " +
    201.                     "object iside a nested structure.");
    202.             }
    203.  
    204.             index++;
    205.         }
    206.  
    207.         EditorGUI.indentLevel--;
    208.         #endregion
    209.     }
    210.  
    211.     /// <summary>
    212.     /// Draws the Background
    213.     /// </summary>
    214.     /// <param name="rect">The Rect where the background is drawn.</param>
    215.     private void DrawBackground (Rect rect)
    216.     {
    217.         switch (BACKGROUND_STYLE) {
    218.  
    219.         case BackgroundStyles.HelpBox:
    220.             EditorGUI.HelpBox (rect, "", MessageType.None);
    221.             break;
    222.  
    223.         case BackgroundStyles.Darken:
    224.             EditorGUI.DrawRect (rect, DARKEN_COLOUR);
    225.             break;
    226.  
    227.         case BackgroundStyles.Lighten:
    228.             EditorGUI.DrawRect (rect, LIGHTEN_COLOUR);
    229.             break;
    230.         }
    231.     }
    232. }
    233.  
    234. /// <summary>
    235. /// Required for the fetching of a default editor on MonoBehaviour objects.
    236. /// </summary>
    237. [CanEditMultipleObjects]
    238. [CustomEditor(typeof(MonoBehaviour), true)]
    239. public class MonoBehaviourEditor : Editor { }
    240.  
    241. /// <summary>
    242. /// Required for the fetching of a default editor on ScriptableObject objects.
    243. /// </summary>
    244. [CanEditMultipleObjects]
    245. [CustomEditor(typeof(ScriptableObject), true)]
    246. public class ScriptableObjectEditor : Editor { }
    247. #endif
     
  13. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    Really helps with readability but there are four issues, three of which have to do with nested [Expandable], I think it's expanding recursively:
    1. Overlap issue in the following condition:
    Code (CSharp):
    1. [CreateAssetMenu]
    2. public class Construct : ScriptableObject {
    3.     public int price;
    4.  
    5.     [Expandable]
    6.     public List<Construct> ingredients;
    7.  
    8.     public List<Elemental> elementals;
    9.  
    10.     [System.Serializable]
    11.     public class PropertySubjectValue
    12.     {
    13.         [Expandable]
    14.         public Property property;
    15.         public Object subject;
    16.         public float value;
    17.     }
    18.     public List<PropertySubjectValue> propertyValues;
    19.  
    20.     [System.Serializable]
    21.     public class ActionActionPair
    22.     {
    23.         public Construct actor;
    24.         [Expandable]
    25.         public Action resultingAction;
    26.     }
    27.     public List<ActionActionPair> actorActions;
    28. }


    and in a mono using the dark theme


    2. it seems to double expand...


    3. it doesn't handle this scenario well

    Code (CSharp):
    1. // break down into smaller components
    2. [CreateAssetMenu(menuName= "Actions/Mine")]
    3. public class ActionMine : Action{
    4.     [Tooltip("unit per seconds")]
    5.     public float efficiency =2.3f;
    6. }
    7.  
    4. in a mono, it shows the collection size even when the collection label is folded
     
    Last edited: Dec 23, 2017
  14. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    2 of those issues (1 and 3) are because you are using another PropertyDrawer to draw the property, which is a condition which I didn't design for. Unfortunately, this cannot be fixed without breaking the encapsulation of the PropertyDrawer.

    Issue number 4 I think is caused because I used "field.NextVisible (true)" instead of "field.NextVisible (false)", so I changed that and now it's fixed.

    I also forgot to remove some debugs, which I have also done.

    The indent size was also off, so I fixed that.

    Issue number 1 is absolutely impossible to fix whilst keeping it optimized using the Rects without breaking encapsulation.

    Issue number 3 could be fixed by fetching the editors width and using that instead of the Rect you are passing it with the PropertyDrawer your using, but that would break other uses for the property drawer.

    Here is the amended code:

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. #if UNITY_EDITOR
    5. using System.Collections.Generic;
    6. using UnityEditor;
    7. #endif
    8.  
    9. /// <summary>
    10. /// Use this property on a ScriptableObject type to allow the editors drawing the field to draw an expandable
    11. /// area that allows for changing the values on the object without having to change editor.
    12. /// </summary>
    13. public class ExpandableAttribute : PropertyAttribute
    14. {
    15.     public ExpandableAttribute ()
    16.     {
    17.  
    18.     }
    19. }
    20.  
    21. #if UNITY_EDITOR
    22. /// <summary>
    23. /// Draws the property field for any field marked with ExpandableAttribute.
    24. /// </summary>
    25. [CustomPropertyDrawer (typeof (ExpandableAttribute), true)]
    26. public class ExpandableAttributeDrawer : PropertyDrawer
    27. {
    28.     // Use the following area to change the style of the expandable ScriptableObject drawers;
    29.     #region Style Setup
    30.     private enum BackgroundStyles
    31.     {
    32.         None,
    33.         HelpBox,
    34.         Darken,
    35.         Lighten
    36.     }
    37.  
    38.     /// <summary>
    39.     /// Whether the default editor Script field should be shown.
    40.     /// </summary>
    41.     private static bool SHOW_SCRIPT_FIELD = false;
    42.  
    43.     /// <summary>
    44.     /// The spacing on the inside of the background rect.
    45.     /// </summary>
    46.     private static float INNER_SPACING = 6.0f;
    47.  
    48.     /// <summary>
    49.     /// The spacing on the outside of the background rect.
    50.     /// </summary>
    51.     private static float OUTER_SPACING = 4.0f;
    52.  
    53.     /// <summary>
    54.     /// The style the background uses.
    55.     /// </summary>
    56.     private static BackgroundStyles BACKGROUND_STYLE = BackgroundStyles.HelpBox;
    57.  
    58.     /// <summary>
    59.     /// The colour that is used to darken the background.
    60.     /// </summary>
    61.     private static Color DARKEN_COLOUR = new Color (0.0f, 0.0f, 0.0f, 0.2f);
    62.  
    63.     /// <summary>
    64.     /// The colour that is used to lighten the background.
    65.     /// </summary>
    66.     private static Color LIGHTEN_COLOUR = new Color (1.0f, 1.0f, 1.0f, 0.2f);
    67.     #endregion
    68.  
    69.     /// <summary>
    70.     /// Cached editor reference.
    71.     /// </summary>
    72.     private Editor editor = null;
    73.  
    74.     public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    75.     {
    76.         float totalHeight = 0.0f;
    77.  
    78.         totalHeight += EditorGUIUtility.singleLineHeight;
    79.  
    80.         if (property.objectReferenceValue == null)
    81.             return totalHeight;
    82.  
    83.         if (!property.isExpanded)
    84.             return totalHeight;
    85.  
    86.         if (editor == null)
    87.             Editor.CreateCachedEditor (property.objectReferenceValue, null, ref editor);
    88.  
    89.         if (editor == null)
    90.             return totalHeight;
    91.        
    92.         SerializedProperty field = editor.serializedObject.GetIterator ();
    93.  
    94.         field.NextVisible (true);
    95.  
    96.         if (SHOW_SCRIPT_FIELD)
    97.         {
    98.             totalHeight += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    99.         }
    100.  
    101.         while (field.NextVisible (false))
    102.         {
    103.             totalHeight += EditorGUI.GetPropertyHeight (field, true) + EditorGUIUtility.standardVerticalSpacing;
    104.         }
    105.  
    106.         totalHeight += INNER_SPACING * 2;
    107.         totalHeight += OUTER_SPACING * 2;
    108.  
    109.         return totalHeight;
    110.     }
    111.  
    112.     public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    113.     {
    114.         Rect fieldRect = new Rect (position);
    115.         fieldRect.height = EditorGUIUtility.singleLineHeight;
    116.  
    117.         EditorGUI.PropertyField (fieldRect, property, label, true);
    118.  
    119.         if (property.objectReferenceValue == null)
    120.             return;
    121.        
    122.         property.isExpanded = EditorGUI.Foldout (fieldRect, property.isExpanded, GUIContent.none, true);
    123.  
    124.         if (!property.isExpanded)
    125.             return;
    126.  
    127.         if (editor == null)
    128.             Editor.CreateCachedEditor (property.objectReferenceValue, null, ref editor);
    129.  
    130.         if (editor == null)
    131.             return;
    132.        
    133.        
    134.         #region Format Field Rects
    135.         List<Rect> propertyRects = new List<Rect> ();
    136.         Rect marchingRect = new Rect (fieldRect);
    137.  
    138.         Rect bodyRect = new Rect (fieldRect);
    139.         bodyRect.xMin += EditorGUI.indentLevel * 14;
    140.         bodyRect.yMin += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing
    141.             + OUTER_SPACING;
    142.        
    143.         SerializedProperty field = editor.serializedObject.GetIterator ();
    144.         field.NextVisible (true);
    145.  
    146.         marchingRect.y += INNER_SPACING + OUTER_SPACING;
    147.  
    148.         if (SHOW_SCRIPT_FIELD)
    149.         {
    150.             propertyRects.Add (marchingRect);
    151.             marchingRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    152.         }
    153.  
    154.         while (field.NextVisible (false))
    155.         {
    156.             marchingRect.y += marchingRect.height + EditorGUIUtility.standardVerticalSpacing;
    157.             marchingRect.height = EditorGUI.GetPropertyHeight (field, true);
    158.             propertyRects.Add (marchingRect);
    159.         }
    160.  
    161.         marchingRect.y += INNER_SPACING;
    162.  
    163.         bodyRect.yMax = marchingRect.yMax;
    164.         #endregion
    165.  
    166.         DrawBackground (bodyRect);
    167.  
    168.         #region Draw Fields
    169.         EditorGUI.indentLevel++;
    170.  
    171.         int index = 0;
    172.         field = editor.serializedObject.GetIterator ();
    173.         field.NextVisible (true);
    174.  
    175.         if (SHOW_SCRIPT_FIELD)
    176.         {
    177.             //Show the disabled script field
    178.             EditorGUI.BeginDisabledGroup (true);
    179.             EditorGUI.PropertyField (propertyRects[index], field, true);
    180.             EditorGUI.EndDisabledGroup ();
    181.             index++;
    182.         }
    183.  
    184.         //Replacement for "editor.OnInspectorGUI ();" so we have more control on how we draw the editor
    185.         while (field.NextVisible (false))
    186.         {
    187.             try
    188.             {
    189.                 EditorGUI.PropertyField (propertyRects[index], field, true);
    190.             }
    191.             catch (StackOverflowException)
    192.             {
    193.                 field.objectReferenceValue = null;
    194.                 Debug.LogError ("Detected self-nesting cauisng a StackOverflowException, avoid using the same " +
    195.                     "object iside a nested structure.");
    196.             }
    197.  
    198.             index++;
    199.         }
    200.  
    201.         EditorGUI.indentLevel--;
    202.         #endregion
    203.     }
    204.  
    205.     /// <summary>
    206.     /// Draws the Background
    207.     /// </summary>
    208.     /// <param name="rect">The Rect where the background is drawn.</param>
    209.     private void DrawBackground (Rect rect)
    210.     {
    211.         switch (BACKGROUND_STYLE) {
    212.  
    213.         case BackgroundStyles.HelpBox:
    214.             EditorGUI.HelpBox (rect, "", MessageType.None);
    215.             break;
    216.  
    217.         case BackgroundStyles.Darken:
    218.             EditorGUI.DrawRect (rect, DARKEN_COLOUR);
    219.             break;
    220.  
    221.         case BackgroundStyles.Lighten:
    222.             EditorGUI.DrawRect (rect, LIGHTEN_COLOUR);
    223.             break;
    224.         }
    225.     }
    226. }
    227.  
    228. /// <summary>
    229. /// Required for the fetching of a default editor on MonoBehaviour objects.
    230. /// </summary>
    231. [CanEditMultipleObjects]
    232. [CustomEditor(typeof(MonoBehaviour), true)]
    233. public class MonoBehaviourEditor : Editor { }
    234.  
    235. /// <summary>
    236. /// Required for the fetching of a default editor on ScriptableObject objects.
    237. /// </summary>
    238. [CanEditMultipleObjects]
    239. [CustomEditor(typeof(ScriptableObject), true)]
    240. public class ScriptableObjectEditor : Editor { }
    241. #endif
     
    luoxiaoc, aka3eka and laurentlavigne like this.
  15. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    I use encapsulation, could these be fixed by using GUILayout instead of rect? In your email you mentioned that it slows things down a lot, it's alright because in-play deep expansion is used for debug purposes anyway, for example "this tree isn't producing any apple, I can see if the production list isn't being updated"
     
  16. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    which uses?
    Maybe a [Compact] attribute can be created to bring the field closer to the label, in which case this can be hacked around.

    Code (CSharp):
    1. public class Compact : PropertyAttribute {public Compact (){}}
    2. [CustomPropertyDrawer (typeof (Compact), true)]
    3. public class CompactDrawer : PropertyDrawer
    4. {
    5.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    6.     {
    7.         Rect rect = new Rect (position);
    8.         rect.height = EditorGUIUtility.singleLineHeight;
    9.  
    10.         EditorGUI.PropertyField (rect, property, GUIContent.none);
    11.     }
    12. }
    I couldn' figure out how to to move the label closer to the field, this is meh-ok for now
     
    Last edited: Dec 23, 2017
  17. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    Yes, GUILayout would fix that - but it would also not allow it to draw in lists properly.

    The currently you use EditorGUI.PropertyField (rect, prop) concerning issue 3, if I instead of using the rect that is passed to the property expand that to the size of the inspector.

    This would involve using reflection to get the Inspector window and then use thats dimensions instead of the ones used to draw the property.

    Then you will just have to add EditorGUI.GetPropertyHeight () to your property that you are using.

    This would work in lists - but it wouldn't work if you wanted to draw multiple of them as children. In this case you would have to rewrite the script.
     
  18. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    That's a real bummer.
    Can you make the expanded property display at the end of the list like in @TheVastBernie's version?
     
  19. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    Yes, it's possible. That's what I mean't by GUILayout not drawing into lists properly.

    It would be very easy to do the GUILayout version and probably be about half the amount of code.

    I'd still always recommend you edit the property drawer you are currently using to include the rect property drawer but GUILayout is a lot easier to impliment.
     
  20. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    I reverted back to @TheVastBernie version and just added an Area
    It's good enough


    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using UnityEditor;
    4.  
    5. [CustomPropertyDrawer(typeof(ScriptableObject), true)]
    6. public class ScriptableObjectDrawer : PropertyDrawer
    7. {
    8.     // Cached scriptable object editor
    9.     Editor editor = null;
    10.  
    11.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    12.     {
    13.         // Draw label
    14.         EditorGUI.PropertyField(position, property, label, true);
    15.  
    16.         // Draw foldout arrow
    17.         if (property.objectReferenceValue != null)
    18.         {
    19.             property.isExpanded = EditorGUI.Foldout(position, property.isExpanded, GUIContent.none);
    20.         }
    21.  
    22.         // Draw foldout properties
    23.         if (property.isExpanded)
    24.         {
    25.             // Make child fields be indented
    26.             EditorGUI.indentLevel++;
    27.  
    28.             // background
    29.             GUILayout.BeginVertical("box");
    30.  
    31.             if (!editor)
    32.                 Editor.CreateCachedEditor(property.objectReferenceValue, null, ref editor);
    33.  
    34.             // Draw object properties
    35.             EditorGUI.BeginChangeCheck();
    36.             if (editor) // catch empty property
    37.             {
    38.                 editor.OnInspectorGUI ();
    39.             }
    40.             if (EditorGUI.EndChangeCheck())
    41.                 property.serializedObject.ApplyModifiedProperties();
    42.  
    43.             GUILayout.EndVertical ();
    44.  
    45.             // Set indent back to what it was
    46.             EditorGUI.indentLevel--;
    47.         }
    48.     }
    49. }
    50.  
    51. [CanEditMultipleObjects]
    52. [CustomEditor(typeof(UnityEngine.Object), true)]
    53. public class UnityObjectEditor : Editor
    54. {
    55. }
     
    Eristen and neshius108 like this.
  21. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    Is there a way to have the original properties be just a template?
    So any change I make in the inspector of the thing that uses those properties as value is only recorded on the thing.
     
  22. Gizmoi

    Gizmoi

    Joined:
    Jan 9, 2013
    Posts:
    327
    I had to add this line to the bottom of OnGUI to get it to save any changes to the ScriptableObject.

    Code (csharp):
    1. editor.serializedObject.ApplyModifiedProperties();
     
  23. steo

    steo

    Joined:
    Aug 10, 2015
    Posts:
    1
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. [CanEditMultipleObjects]
    4. [CustomEditor(typeof(UnityEngine.Object), true)]
    5. public class UnityObjectEditor : Editor
    6. {
    7. }
    This code helps to simply draw any ScriptabeObject or Monobehaviour inside your property drawers, but it slowdown every other property drawer in the project by passing EventType.Layout into PropertyDrawer.OnGUI. Really, every property drawer in project becomes twice as slow.

    @Fydar, why do you need custom editors for MonoBehaviour and ScriptableObject? They are unnesessary and you code works without them.
     
  24. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    @steo Yes, I should have just used UnityEngine.Object instead, much cleaner.

    I need to use this default property drawer because Editor.CreateCachedEditor will return null if there are bo user defined editors. I need to get the Edtior.serializedObject to iterate over the fields.

    From what I have tested in the versions of Unity that I have, these were nessessary to prevent the property drawer from erroringwhen drawing objects that don't have a custom editor.
     
  25. Gizmoi

    Gizmoi

    Joined:
    Jan 9, 2013
    Posts:
    327
    Regarding Editor.CreateCachedEditor to get the SerializedObject; could you not simply use
    Code (csharp):
    1. new SerializedObject(property)
    instead?
     
  26. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    Interesting theory, @Gizmoi, that would definately be a lot more efficient, though I might have to impliment a cache of my own to avoid calling it when unnecessary to do so.

    I never looked at the constructor for SerializedObject, but I have a feeling it's new SerializedObject (property.objectReferenceValue); rather than the property, but at this point I have no idea.

    If it does give the correct SerializedObject then yes it would definately work.
     
  27. Gizmoi

    Gizmoi

    Joined:
    Jan 9, 2013
    Posts:
    327
    Yes, you are right, property.objectReferenceValue is what you want.
    Can you safely cache SerializedObjects? Shouldn't they be reserialised to make sure the data is up to date?
     
  28. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    Thanks, @Gizmoi, I have produced an updated version of the ExpandableAttribute that uses "new SerializedObject (property.objectReference)" instead of Editor.CreateCachedEditor.

    The code is as shown below. It is much more optimized than before, especially because it doesn't create "dummy" PropertyDrawers.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. #if UNITY_EDITOR
    4. using System;
    5. using System.Collections.Generic;
    6. using UnityEditor;
    7. #endif
    8.  
    9. /// <summary>
    10. /// Use this property on a ScriptableObject type to allow the editors drawing the field to draw an expandable
    11. /// area that allows for changing the values on the object without having to change editor.
    12. /// </summary>
    13. public class ExpandableAttribute : PropertyAttribute
    14. {
    15.     public ExpandableAttribute ()
    16.     {
    17.  
    18.     }
    19. }
    20.  
    21. #if UNITY_EDITOR
    22. /// <summary>
    23. /// Draws the property field for any field marked with ExpandableAttribute.
    24. /// </summary>
    25. [CustomPropertyDrawer (typeof (ExpandableAttribute), true)]
    26. public class ExpandableAttributeDrawer : PropertyDrawer
    27. {
    28.     // Use the following area to change the style of the expandable ScriptableObject drawers;
    29.     #region Style Setup
    30.     private enum BackgroundStyles
    31.     {
    32.         None,
    33.         HelpBox,
    34.         Darken,
    35.         Lighten
    36.     }
    37.  
    38.     /// <summary>
    39.     /// Whether the default editor Script field should be shown.
    40.     /// </summary>
    41.     private static bool SHOW_SCRIPT_FIELD = false;
    42.  
    43.     /// <summary>
    44.     /// The spacing on the inside of the background rect.
    45.     /// </summary>
    46.     private static float INNER_SPACING = 6.0f;
    47.  
    48.     /// <summary>
    49.     /// The spacing on the outside of the background rect.
    50.     /// </summary>
    51.     private static float OUTER_SPACING = 4.0f;
    52.  
    53.     /// <summary>
    54.     /// The style the background uses.
    55.     /// </summary>
    56.     private static BackgroundStyles BACKGROUND_STYLE = BackgroundStyles.HelpBox;
    57.  
    58.     /// <summary>
    59.     /// The colour that is used to darken the background.
    60.     /// </summary>
    61.     private static Color DARKEN_COLOUR = new Color (0.0f, 0.0f, 0.0f, 0.2f);
    62.  
    63.     /// <summary>
    64.     /// The colour that is used to lighten the background.
    65.     /// </summary>
    66.     private static Color LIGHTEN_COLOUR = new Color (1.0f, 1.0f, 1.0f, 0.2f);
    67.     #endregion
    68.  
    69.     public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    70.     {
    71.         float totalHeight = 0.0f;
    72.  
    73.         totalHeight += EditorGUIUtility.singleLineHeight;
    74.  
    75.         if (property.objectReferenceValue == null)
    76.             return totalHeight;
    77.  
    78.         if (!property.isExpanded)
    79.             return totalHeight;
    80.  
    81.         SerializedObject targetObject = new SerializedObject (property.objectReferenceValue);
    82.  
    83.         if (targetObject == null)
    84.             return totalHeight;
    85.      
    86.         SerializedProperty field = targetObject.GetIterator ();
    87.  
    88.         field.NextVisible (true);
    89.  
    90.         if (SHOW_SCRIPT_FIELD)
    91.         {
    92.             totalHeight += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    93.         }
    94.  
    95.         while (field.NextVisible (false))
    96.         {
    97.             totalHeight += EditorGUI.GetPropertyHeight (field, true) + EditorGUIUtility.standardVerticalSpacing;
    98.         }
    99.  
    100.         totalHeight += INNER_SPACING * 2;
    101.         totalHeight += OUTER_SPACING * 2;
    102.  
    103.         return totalHeight;
    104.     }
    105.  
    106.     public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    107.     {
    108.         Rect fieldRect = new Rect (position);
    109.         fieldRect.height = EditorGUIUtility.singleLineHeight;
    110.  
    111.         EditorGUI.PropertyField (fieldRect, property, label, true);
    112.  
    113.         if (property.objectReferenceValue == null)
    114.             return;
    115.      
    116.         property.isExpanded = EditorGUI.Foldout (fieldRect, property.isExpanded, GUIContent.none, true);
    117.  
    118.         if (!property.isExpanded)
    119.             return;
    120.  
    121.         SerializedObject targetObject = new SerializedObject (property.objectReferenceValue);
    122.  
    123.         if (targetObject == null)
    124.             return;
    125.      
    126.      
    127.         #region Format Field Rects
    128.         List<Rect> propertyRects = new List<Rect> ();
    129.         Rect marchingRect = new Rect (fieldRect);
    130.  
    131.         Rect bodyRect = new Rect (fieldRect);
    132.         bodyRect.xMin += EditorGUI.indentLevel * 14;
    133.         bodyRect.yMin += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing
    134.             + OUTER_SPACING;
    135.      
    136.         SerializedProperty field = targetObject.GetIterator ();
    137.         field.NextVisible (true);
    138.  
    139.         marchingRect.y += INNER_SPACING + OUTER_SPACING;
    140.  
    141.         if (SHOW_SCRIPT_FIELD)
    142.         {
    143.             propertyRects.Add (marchingRect);
    144.             marchingRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    145.         }
    146.  
    147.         while (field.NextVisible (false))
    148.         {
    149.             marchingRect.y += marchingRect.height + EditorGUIUtility.standardVerticalSpacing;
    150.             marchingRect.height = EditorGUI.GetPropertyHeight (field, true);
    151.             propertyRects.Add (marchingRect);
    152.         }
    153.  
    154.         marchingRect.y += INNER_SPACING;
    155.  
    156.         bodyRect.yMax = marchingRect.yMax;
    157.         #endregion
    158.  
    159.         DrawBackground (bodyRect);
    160.  
    161.         #region Draw Fields
    162.         EditorGUI.indentLevel++;
    163.  
    164.         int index = 0;
    165.         field = targetObject.GetIterator ();
    166.         field.NextVisible (true);
    167.  
    168.         if (SHOW_SCRIPT_FIELD)
    169.         {
    170.             //Show the disabled script field
    171.             EditorGUI.BeginDisabledGroup (true);
    172.             EditorGUI.PropertyField (propertyRects[index], field, true);
    173.             EditorGUI.EndDisabledGroup ();
    174.             index++;
    175.         }
    176.  
    177.         //Replacement for "editor.OnInspectorGUI ();" so we have more control on how we draw the editor
    178.         while (field.NextVisible (false))
    179.         {
    180.             try
    181.             {
    182.                 EditorGUI.PropertyField (propertyRects[index], field, true);
    183.             }
    184.             catch (StackOverflowException)
    185.             {
    186.                 field.objectReferenceValue = null;
    187.                 Debug.LogError ("Detected self-nesting cauisng a StackOverflowException, avoid using the same " +
    188.                     "object iside a nested structure.");
    189.             }
    190.  
    191.             index++;
    192.         }
    193.  
    194.         targetObject.ApplyModifiedProperties ();
    195.  
    196.         EditorGUI.indentLevel--;
    197.         #endregion
    198.     }
    199.  
    200.     /// <summary>
    201.     /// Draws the Background
    202.     /// </summary>
    203.     /// <param name="rect">The Rect where the background is drawn.</param>
    204.     private void DrawBackground (Rect rect)
    205.     {
    206.         switch (BACKGROUND_STYLE) {
    207.  
    208.         case BackgroundStyles.HelpBox:
    209.             EditorGUI.HelpBox (rect, "", MessageType.None);
    210.             break;
    211.  
    212.         case BackgroundStyles.Darken:
    213.             EditorGUI.DrawRect (rect, DARKEN_COLOUR);
    214.             break;
    215.  
    216.         case BackgroundStyles.Lighten:
    217.             EditorGUI.DrawRect (rect, LIGHTEN_COLOUR);
    218.             break;
    219.         }
    220.     }
    221. }
    222. #endif
     
    Eristen, Mesisos, MiguelLobo and 14 others like this.
  29. Quadropups

    Quadropups

    Joined:
    May 23, 2017
    Posts:
    18
    MrDizzle26 likes this.
  30. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,222
    And to use @Fydar new script with assembly definitions or non Editor scripts, put this in an Attributes folder
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. /// <summary>
    4. /// Use this property on a ScriptableObject type to allow the editors drawing the field to draw an expandable
    5. /// area that allows for changing the values on the object without having to change editor.
    6. /// </summary>
    7. public class ExpandableAttribute : PropertyAttribute
    8. {
    9.     public ExpandableAttribute()
    10.     {
    11.  
    12.     }
    13. }
    and that in an Editor folder

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. #if UNITY_EDITOR
    4. using System;
    5. using System.Collections.Generic;
    6. using UnityEditor;
    7. #endif
    8.  
    9. #if UNITY_EDITOR
    10. /// <summary>
    11. /// Draws the property field for any field marked with ExpandableAttribute.
    12. /// </summary>
    13. [CustomPropertyDrawer(typeof(ExpandableAttribute), true)]
    14. public class ExpandableAttributeDrawer : PropertyDrawer
    15. {
    16.     // Use the following area to change the style of the expandable ScriptableObject drawers;
    17.     #region Style Setup
    18.     private enum BackgroundStyles
    19.     {
    20.         None,
    21.         HelpBox,
    22.         Darken,
    23.         Lighten
    24.     }
    25.  
    26.     /// <summary>
    27.     /// Whether the default editor Script field should be shown.
    28.     /// </summary>
    29.     private static bool SHOW_SCRIPT_FIELD = false;
    30.  
    31.     /// <summary>
    32.     /// The spacing on the inside of the background rect.
    33.     /// </summary>
    34.     private static float INNER_SPACING = 6.0f;
    35.  
    36.     /// <summary>
    37.     /// The spacing on the outside of the background rect.
    38.     /// </summary>
    39.     private static float OUTER_SPACING = 4.0f;
    40.  
    41.     /// <summary>
    42.     /// The style the background uses.
    43.     /// </summary>
    44.     private static BackgroundStyles BACKGROUND_STYLE = BackgroundStyles.HelpBox;
    45.  
    46.     /// <summary>
    47.     /// The colour that is used to darken the background.
    48.     /// </summary>
    49.     private static Color DARKEN_COLOUR = new Color(0.0f, 0.0f, 0.0f, 0.2f);
    50.  
    51.     /// <summary>
    52.     /// The colour that is used to lighten the background.
    53.     /// </summary>
    54.     private static Color LIGHTEN_COLOUR = new Color(1.0f, 1.0f, 1.0f, 0.2f);
    55.     #endregion
    56.  
    57.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    58.     {
    59.         float totalHeight = 0.0f;
    60.  
    61.         totalHeight += EditorGUIUtility.singleLineHeight;
    62.  
    63.         if (property.objectReferenceValue == null)
    64.             return totalHeight;
    65.  
    66.         if (!property.isExpanded)
    67.             return totalHeight;
    68.  
    69.         SerializedObject targetObject = new SerializedObject(property.objectReferenceValue);
    70.  
    71.         if (targetObject == null)
    72.             return totalHeight;
    73.  
    74.         SerializedProperty field = targetObject.GetIterator();
    75.  
    76.         field.NextVisible(true);
    77.  
    78.         if (SHOW_SCRIPT_FIELD)
    79.         {
    80.             totalHeight += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    81.         }
    82.  
    83.         while (field.NextVisible(false))
    84.         {
    85.             totalHeight += EditorGUI.GetPropertyHeight(field, true) + EditorGUIUtility.standardVerticalSpacing;
    86.         }
    87.  
    88.         totalHeight += INNER_SPACING * 2;
    89.         totalHeight += OUTER_SPACING * 2;
    90.  
    91.         return totalHeight;
    92.     }
    93.  
    94.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    95.     {
    96.         Rect fieldRect = new Rect(position);
    97.         fieldRect.height = EditorGUIUtility.singleLineHeight;
    98.  
    99.         EditorGUI.PropertyField(fieldRect, property, label, true);
    100.  
    101.         if (property.objectReferenceValue == null)
    102.             return;
    103.  
    104.         property.isExpanded = EditorGUI.Foldout(fieldRect, property.isExpanded, GUIContent.none, true);
    105.  
    106.         if (!property.isExpanded)
    107.             return;
    108.  
    109.         SerializedObject targetObject = new SerializedObject(property.objectReferenceValue);
    110.  
    111.         if (targetObject == null)
    112.             return;
    113.  
    114.  
    115.         #region Format Field Rects
    116.         List<Rect> propertyRects = new List<Rect>();
    117.         Rect marchingRect = new Rect(fieldRect);
    118.  
    119.         Rect bodyRect = new Rect(fieldRect);
    120.         bodyRect.xMin += EditorGUI.indentLevel * 14;
    121.         bodyRect.yMin += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing
    122.             + OUTER_SPACING;
    123.  
    124.         SerializedProperty field = targetObject.GetIterator();
    125.         field.NextVisible(true);
    126.  
    127.         marchingRect.y += INNER_SPACING + OUTER_SPACING;
    128.  
    129.         if (SHOW_SCRIPT_FIELD)
    130.         {
    131.             propertyRects.Add(marchingRect);
    132.             marchingRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    133.         }
    134.  
    135.         while (field.NextVisible(false))
    136.         {
    137.             marchingRect.y += marchingRect.height + EditorGUIUtility.standardVerticalSpacing;
    138.             marchingRect.height = EditorGUI.GetPropertyHeight(field, true);
    139.             propertyRects.Add(marchingRect);
    140.         }
    141.  
    142.         marchingRect.y += INNER_SPACING;
    143.  
    144.         bodyRect.yMax = marchingRect.yMax;
    145.         #endregion
    146.  
    147.         DrawBackground(bodyRect);
    148.  
    149.         #region Draw Fields
    150.         EditorGUI.indentLevel++;
    151.  
    152.         int index = 0;
    153.         field = targetObject.GetIterator();
    154.         field.NextVisible(true);
    155.  
    156.         if (SHOW_SCRIPT_FIELD)
    157.         {
    158.             //Show the disabled script field
    159.             EditorGUI.BeginDisabledGroup(true);
    160.             EditorGUI.PropertyField(propertyRects[index], field, true);
    161.             EditorGUI.EndDisabledGroup();
    162.             index++;
    163.         }
    164.  
    165.         //Replacement for "editor.OnInspectorGUI ();" so we have more control on how we draw the editor
    166.         while (field.NextVisible(false))
    167.         {
    168.             try
    169.             {
    170.                 EditorGUI.PropertyField(propertyRects[index], field, true);
    171.             }
    172.             catch (StackOverflowException)
    173.             {
    174.                 field.objectReferenceValue = null;
    175.                 Debug.LogError("Detected self-nesting cauisng a StackOverflowException, avoid using the same " +
    176.                     "object iside a nested structure.");
    177.             }
    178.  
    179.             index++;
    180.         }
    181.  
    182.         targetObject.ApplyModifiedProperties();
    183.  
    184.         EditorGUI.indentLevel--;
    185.         #endregion
    186.     }
    187.  
    188.     /// <summary>
    189.     /// Draws the Background
    190.     /// </summary>
    191.     /// <param name="rect">The Rect where the background is drawn.</param>
    192.     private void DrawBackground(Rect rect)
    193.     {
    194.         switch (BACKGROUND_STYLE)
    195.         {
    196.  
    197.             case BackgroundStyles.HelpBox:
    198.                 EditorGUI.HelpBox(rect, "", MessageType.None);
    199.                 break;
    200.  
    201.             case BackgroundStyles.Darken:
    202.                 EditorGUI.DrawRect(rect, DARKEN_COLOUR);
    203.                 break;
    204.  
    205.             case BackgroundStyles.Lighten:
    206.                 EditorGUI.DrawRect(rect, LIGHTEN_COLOUR);
    207.                 break;
    208.         }
    209.     }
    210. }
    211. #endif
    212.  
     
    Eristen, LucidSloth, Juaner_ and 8 others like this.
  31. JFortunato

    JFortunato

    Joined:
    Dec 11, 2012
    Posts:
    2
    I'm sorry if I missed, but how can i use the same Scriptable Object on different objects and different values on the attributes?
     
  32. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    @Juninho Hello, I'm not too sure what you mean. However, I believe what your are tackling is quite similar to something I wanted to accomplish in the past.

    If you looking for the ability to get multiple serialised data sets from a single ScriptableObject then that's beyond the capabilities of ScriptableObjects. The above scripts are only capable of sharing a single instance of the ScriptableObject between all of the fields.

    It becomes quite challenging when you want to serialize a secondary set of data in conjunction with a ScriptableObject, however I recommend storing "Metadata" in a property which references the ScriptableObject. You could create this metadata as a class and perhaps use a Serializer like JSONUtility (built into Unity).

    Short of creating a new instance of the ScriptableObject, there is no simple solution.
     
    Eristen likes this.
  33. Deleted User

    Deleted User

    Guest

    thanks! this is BEAST! , like MEGA BEAST :)
     
    Last edited by a moderator: May 18, 2018
  34. Deeeds

    Deeeds

    Joined:
    Mar 15, 2018
    Posts:
    739
    My apologies, I don't understand how to use this, but want to, specifically, I'm stuck understanding this:

    /// Use this property on a ScriptableObject type to allow the editors drawing the field to draw an expandable
    /// area that allows for changing the values on the object without having to change editor.


    Where/how do use this property on a ScriptableObject type?
     
  35. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    Say I have a script that I want to use this on.

    Where I reference the ScriptableObject in the script, I add the ExpandableAttribute.

    Code (CSharp):
    1. [Expandable]
    2. public ScriptableObject renameMe;
     
    Eristen and Deeeds like this.
  36. fidelsoto

    fidelsoto

    Joined:
    Aug 7, 2012
    Posts:
    87
    Wow. This is pretty good! Thank you very much guys!
     
  37. IceSentry

    IceSentry

    Joined:
    Nov 16, 2015
    Posts:
    1
    @Fydar did you release this on the asset store? I see you mentioned releasing it in the future, but you never gave a link to a github or asset store, so I'm not sure if I should copy the code here or if you have an actual asset
     
  38. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    @IceSentry I never found anything that needed yo be done to it so I never did anything like that :)

    It was definately the intention but it was great how it was :)

    No need to overcomplicate it ;)

    EDIT: on second thought - it could do with caching the SerializedObiects - but I guess just posting a reply here will be enough :)
     
    NeatWolf, MaDDoX, L-box and 3 others like this.
  39. L-box

    L-box

    Joined:
    Oct 29, 2018
    Posts:
    2
    I will simply post this out there;
    I do not know if theres another version but it work just fine.

    100% badass!
    One love.
     
  40. TeotiGraphix

    TeotiGraphix

    Joined:
    Jan 11, 2011
    Posts:
    145
    @Fydar This is a great script, thank you to all that pitched in on this, saves so much time.
     
    MaDDoX likes this.
  41. CSmochinaArnia

    CSmochinaArnia

    Joined:
    Feb 26, 2018
    Posts:
    7
    Awesome work guys.
    What would I need to change or add so that a reference to a prefab or gameobject that has a scriptable object referenced, can also be expandable?
     
  42. L-box

    L-box

    Joined:
    Oct 29, 2018
    Posts:
    2
    laurentlavigne likes this.
  43. NeatWolf

    NeatWolf

    Joined:
    Sep 27, 2013
    Posts:
    924
    Hi there!

    Just curious, have you (or anyone) ever uploaded this on GitHub?
    Is this Odin compatible? (or, probably, Odin already has support for it?)

    I guess the original creator is not even following the thread anymore, quite some time has passed since the last reply, and I"m not even sure the dropbox link is still working :D

    @laurentlavigne , @Fydar , do you have an up to date version to share? :)

    I'd really appreciate if someone had it loaded or uploaded somewhere in the meantime :)
     
  44. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    I think my most recent comment stands ^^^

    I haven't seen any changes to make yo make it worth uploading the code elsewhere :)

    As far as I'm aware it works with Unity 2019.
     
    NeatWolf likes this.
  45. NeatWolf

    NeatWolf

    Joined:
    Sep 27, 2013
    Posts:
    924
    Thanks!
    Glad to know, I just fear it could overlap to an Attribute from Odin Inspector.
    Which, btw, doesn't offer the same neat auto-colorizing options you've got there :)

    I generally create one [Serializable] class such as "MyData", and use it as a field both in the ScriptableObject (reference data not meant to be changed) and as a field/property in the class I'm going to reference the SO as well.

    So, in MyBehaviour I'm going to have:

    - a reference to a static set of values, statically saved in a MyData field of the SO
    - a MyData field, containing the actual runtime data of that particular instance.

    You could also implement the... IComparable (if I remember it right) interface on the MyData Class, to allow comparison, assignments, and so on.

    Of course, using a Struct could solve all of your problems.

    Too bad Unity doesn't support "default" class properties, otherwise you could reference SO.someFieldinMyData directly.
     
    CodeRonnie likes this.
  46. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    Oh I have absolutely no idea how it interacts with Odin Inspector xD

    I don't use it, so I haven't had the opportunity to test it.
     
  47. myzzie

    myzzie

    Joined:
    Feb 19, 2015
    Posts:
    13
    Found a small mistake when enabling script field

    Code (CSharp):
    1.         if (SHOW_SCRIPT_FIELD)
    2.         {
    3.             propertyRects.Add (marchingRect);
    4.             marchingRect.y += EditorGUIUtility.singleLineHeight +
    5.             EditorGUIUtility.standardVerticalSpacing;
    6.         }
    Just switch them around and the disabled script field should display correctly.
     
  48. Natpokey

    Natpokey

    Joined:
    Oct 8, 2018
    Posts:
    2
    I have a small question, I really love this feature but it doesn't seem to work for me when I have custom editors of scriptable objects. I sort of understand why, but I don't want this problem preventing me from using such an incredibly awesome feature. Does anyone have any good pointers as to how I could potentially attempt a fix?
     
  49. Fydar

    Fydar

    Joined:
    Feb 23, 2014
    Posts:
    70
    @Natpokey Some of the earlier scripts posted here actually instantiated an editor and then rendered the fields, the one I posted used ScriptableObjects and then iterated over them like Unity's default editor does.

    Perhaps a combination of some of the earlier versions might get you what you're looking for.
     
    CodeRonnie likes this.
  50. dwarfengine

    dwarfengine

    Joined:
    Aug 3, 2019
    Posts:
    13
    @Fydar hey man, I see that you're still around here, so I thought I should tag you. I was looking for a solution like this and saw this thread. I tried to use the code in the main post. It works, but when I expand a ScriptableObject inside the list, the properties of that SO appears at the bottom of the list. How do I make it so it expands right below the element itself? (I'm trying to make a simple Behaviour Tree editor.)



    EDIT: It seems that I skipped your comments. Now it works like expected. Thanks for your solution!

    Also I sent a mail about using it in an open source project.
     
    Last edited: Mar 26, 2020