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

Better ScriptableObjects Inspector-Editing (Editor Tool)

Discussion in 'Scripting' 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 UnityEngine;
    2. using UnityEditor;
    3.  
    4. [CustomPropertyDrawer(typeof(ScriptableObject), true)]
    5. public class ScriptableObjectDrawer : PropertyDrawer
    6. {
    7.     // Cached scriptable object editor
    8.     private Editor editor = null;
    9.  
    10.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    11.     {
    12.         // Draw label
    13.         EditorGUI.PropertyField(position, property, label, true);
    14.        
    15.         // Draw foldout arrow
    16.         if (property.objectReferenceValue != null)
    17.         {
    18.             property.isExpanded = EditorGUI.Foldout(position, property.isExpanded, GUIContent.none);
    19.         }
    20.  
    21.         // Draw foldout properties
    22.         if (property.isExpanded)
    23.         {
    24.             // Make child fields be indented
    25.             EditorGUI.indentLevel++;
    26.            
    27.             // Draw object properties
    28.             if (!editor)
    29.                 Editor.CreateCachedEditor(property.objectReferenceValue, null, ref editor);
    30.             editor.OnInspectorGUI();
    31.            
    32.             // Set indent back to what it was
    33.             EditorGUI.indentLevel--;
    34.         }
    35.     }
    36. }
    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
    noio, quyrean, jam-slc and 4 others like this.
  2. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,539
    Didn't have a chance to try it until now, but seems to work perfectly! Definitely recommend others give it a try, it's simple and speeds up workflow quite a bit, just put it in your project and you're good to go. We use scriptable objects quite heavily so this is going to be very helpful.

    @TheVastBernie I would recommend attaching a zip of those two script files in their .cs format to make it a bit simpler for people to try it out!
     
  3. TheVastBernie

    TheVastBernie

    Joined:
    Mar 25, 2014
    Posts:
    10
    Thanks for the suggestion, I added a unitypackage for easier import.
    Also I fixed a few quirks with multi-object editing of MonoBehaviours and a bug causing labels to draw twice.
    I added everything under the main post
     
  4. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,539
    Hey, after using this for a while I founding one lacking issue with it. The ScriptableObject's "OnValidate()" isn't propagated to the script its being displayed in. So when you modify its values you don't get that callback to make things update to those changes.

    My solution was to add:
    Code (CSharp):
    1. MonoBehaviour mono = property.serializedObject.targetObject as MonoBehaviour;
    2.             if(mono != null){
    3.                     mono.Invoke("OnValidate", 0);
    4.             }
    Just before the final "EditorGUI.indentLevel--;" line. This works for the most part, though it calls OnValidate a bunch of times on each change. If you can think of a better option would love to hear it!

    (Had to using Invoke instead of SendMessage because SendMessage requires the Mono script to be "ExecuteInEditMode" for it to work out of PlayMode, otherwise you get an exception.)
     
  5. quyrean

    quyrean

    Joined:
    Nov 29, 2016
    Posts:
    16
    This is really cool, thanks.

    I added the following script so that I can use the inspector for scriptable object assets from the Project menu. Without it, I was getting an error.

    ArgumentException: Getting control 4's position in a group with only 4 controls when doing Repaint
    Aborting



    ScriptableObjectEditor.cs

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. /// https://forum.unity.com/threads/better-scriptableobjects-inspector-editing-editor-tool.484392/
    5. ///see ScriptableObjectDrawer
    6. [CanEditMultipleObjects]
    7. [CustomEditor(typeof(ScriptableObject), true)]
    8. public class ScriptableObjectEditor : Editor
    9. {
    10. }
    11.  
     
  6. joshcamas

    joshcamas

    Joined:
    Jun 16, 2017
    Posts:
    1,276
    This is absolutely brilliant!
     
  7. Keysawn

    Keysawn

    Joined:
    Sep 1, 2018
    Posts:
    4
    This is amazing... I would have paid for it on the Asset Store, but I'm very glad you shared it! Thank you.

    EDIT: I found out that with an array of ScriptableObjects, clicking on any of the "expand" arrows actually expand all ScriptableObjects of the array, displaying all of them at the end of the list. Weird behavior, that I have no idea how to solve...
     
    Last edited: May 4, 2019
  8. noio

    noio

    Joined:
    Dec 17, 2013
    Posts:
    226
    @TheVastBernie I assume this is the bug you were working around? http://answers.unity.com/answers/1422999/view.html

    Also, the layout events seem not to work on CustomPropertyDrawers on a PropertyAttribute.

    My ideal scenario would be to use something like below, with the attribute determining if the ScriptableObject is inlined.

    Code (CSharp):
    1. [InlineScriptableObject] public PlayerBaseStats Stats;
    It seems that there have been multiple attempts at creating this feature, but none are quite perfect.
     
    Last edited: Nov 25, 2019
    hamza_unity995 and bobbaluba like this.
  9. LukasSacher

    LukasSacher

    Joined:
    May 2, 2019
    Posts:
    14
    @Invertex I found a more performant way to call OnValidate. I need this since my OnValidate generates a mesh and doing this multiple times each frame causes my editor to run at 15fps.
    I added:
    Code (CSharp):
    1. var mono = property.serializedObject.targetObject as MonoBehaviour;
    2.             if(property.objectReferenceValue is IEmbeddable so && mono != null)
    3.                 so.InvokeOnValidate(mono, "OnValidate");
    just before the indentLevel-- line and made sure my Scriptable Object implemented the Interface:
    Code (CSharp):
    1. public interface IEmbeddable
    2. {
    3.     public void InvokeOnValidate(MonoBehaviour mono, string methodName);
    4. }
    like this:
    Code (CSharp):
    1. private void OnValidate()
    2.     {
    3.         if (_monoOnNextValidate != null)
    4.             _monoOnNextValidate.Invoke(_nameOnNextValidate, 0);
    5.     }
    6.  
    7.     private MonoBehaviour _monoOnNextValidate;
    8.     private string _nameOnNextValidate;
    9.     public void InvokeOnValidate(MonoBehaviour mono, string methodName)
    10.     {
    11.         _monoOnNextValidate = mono;
    12.         _nameOnNextValidate = methodName;
    13.     }
    It's a half-decent workaround but it works if you have to prevent OnValidate from being called multiple times. So if you can, go for the previous solution.
    Hope it helps!