Property Drawers & Custom Inspectors

Checked with version: 4.6

-

Difficulty: Intermediate

As an introduction to editor scripting, in the session we will look at how to customize and change the way the Inspector looks in our project by using Property Drawers and Custom Inspectors. Property Drawers allow us to customize the way certain controls or a custom class look in the inspector. Custom Inspectors allow us to change the look of specific components in the inspector. We will show you how to do this. Tutor - Adam Buckner

Property Drawers & Custom Inspectors

Intermediate Scripting

Additional Links being added asap...

This script, TransformInspector, is no longer needed as a custom inspector, as noted in the lesson, but it still serves as a solid lesson in Editor Scripting.

Transform Inspector

Code snippet

using UnityEngine;
using UnityEditor;
using System.Collections;

[CustomEditor(typeof(Transform))]
public class TransformInspector : Editor {
    
    public bool showTools;
    public bool copyPosition;
    public bool copyRotation;
    public bool copyScale;
    public bool pastePosition;
    public bool pasteRotation;
    public bool pasteScale;
    public bool selectionNullError;
    
    public override void OnInspectorGUI() {

        Transform t = (Transform)target;

        // Replicate the standard transform inspector gui
        EditorGUIUtility.LookLikeControls();
        EditorGUI.indentLevel = 0;
        Vector3 position = EditorGUILayout.Vector3Field("Position", t.localPosition);
        Vector3 eulerAngles = EditorGUILayout.Vector3Field("Rotation", t.localEulerAngles);
        Vector3 scale = EditorGUILayout.Vector3Field("Scale", t.localScale);
        EditorGUIUtility.LookLikeInspector();
        
        //
        if (GUILayout.Button ((showTools) ? "Hide Transform Tools" : "Show Transform Tools")) {
            showTools = !showTools;
            EditorPrefs.SetBool ("ShowTools", showTools);
        }
        //  START TRANSFORM TOOLS FOLD DOWN //
        if (showTools) {
            if (!copyPosition && !copyRotation && !copyScale) {
                selectionNullError = true;
            } else {
                selectionNullError = false;
            }
            EditorGUILayout.BeginHorizontal ();
            if (GUILayout.Button (selectionNullError ? "Nothing Selected" : "Copy Transform")) {
                if (copyPosition) {
                    EditorPrefs.SetFloat("LocalPosX", t.localPosition.x);
                    EditorPrefs.SetFloat("LocalPosY", t.localPosition.y);
                    EditorPrefs.SetFloat("LocalPosZ", t.localPosition.z);
                }
                if (copyRotation) {
                    EditorPrefs.SetFloat("LocalRotX", t.localEulerAngles.x);
                    EditorPrefs.SetFloat("LocalRotY", t.localEulerAngles.y);
                    EditorPrefs.SetFloat("LocalRotZ", t.localEulerAngles.z);
                }
                if (copyScale) {
                    EditorPrefs.SetFloat("LocalScaleX", t.localScale.x);
                    EditorPrefs.SetFloat("LocalScaleY", t.localScale.y);
                    EditorPrefs.SetFloat("LocalScaleZ", t.localScale.z);
                }
                
                Debug.Log("LP: " + t.localPosition + " LT: (" + t.localEulerAngles.x + ", " + t.localEulerAngles.y + ", " + t.localEulerAngles.z + ") LS: " + t.localScale);
            }
            if (GUILayout.Button ("Paste Transform")) {
                Vector3 tV3 = new Vector3();
                if (pastePosition) {
                    tV3.x = EditorPrefs.GetFloat("LocalPosX", 0.0f);
                    tV3.y = EditorPrefs.GetFloat("LocalPosY", 0.0f);
                    tV3.z = EditorPrefs.GetFloat("LocalPosZ", 0.0f);
                    t.localPosition = tV3;
                }
                if (pasteRotation) {
                    tV3.x = EditorPrefs.GetFloat("LocalRotX", 0.0f);
                    tV3.y = EditorPrefs.GetFloat("LocalRotY", 0.0f);
                    tV3.z = EditorPrefs.GetFloat("LocalRotZ", 0.0f);
                    t.localEulerAngles = tV3;
                }
                if (pasteScale) {
                    tV3.x = EditorPrefs.GetFloat("LocalScaleX", 1.0f);
                    tV3.y = EditorPrefs.GetFloat("LocalScaleY", 1.0f);
                    tV3.z = EditorPrefs.GetFloat("LocalScaleZ", 1.0f);
                    t.localScale = tV3;
                }
                
                Debug.Log("LP: " + t.localPosition + " LT: " + t.localEulerAngles + " LS: " + t.localScale);
            }
            EditorGUILayout.EndHorizontal ();
        
            EditorGUIUtility.LookLikeControls();
            EditorGUILayout.BeginHorizontal();
            GUILayout.Label ("Position", GUILayout.Width (75));
            GUILayout.Label ("Rotation", GUILayout.Width (75));
            GUILayout.Label ("Scale", GUILayout.Width (50));
            if (GUILayout.Button ("All", GUILayout.MaxWidth (40))) TransformCopyAll ();
            EditorGUILayout.EndHorizontal();
            EditorGUILayout.BeginHorizontal();
            GUILayout.Space(20);
            copyPosition = EditorGUILayout.Toggle (copyPosition, GUILayout.Width (75));
            copyRotation = EditorGUILayout.Toggle (copyRotation, GUILayout.Width (65));
            copyScale = EditorGUILayout.Toggle (copyScale, GUILayout.Width (45));
            if (GUILayout.Button ("None", GUILayout.MaxWidth (40))) TransformCopyNone ();
            EditorGUILayout.EndHorizontal();
            EditorGUIUtility.LookLikeInspector();
            }
        //  END TRANSFORM TOOLS FOLD DOWN   //

        if (GUI.changed) {
            SetCopyPasteBools ();
            Undo.RegisterUndo(t, "Transform Change");

            t.localPosition = FixIfNaN(position);
            t.localEulerAngles = FixIfNaN(eulerAngles);
            t.localScale = FixIfNaN(scale);
        }
    }

    private Vector3 FixIfNaN(Vector3 v) {
        if (float.IsNaN(v.x)) {
            v.x = 0;
        }
        if (float.IsNaN(v.y)) {
            v.y = 0;
        }
        if (float.IsNaN(v.z)) {
            v.z = 0;
        }
        return v;
    }
    
    void OnEnable () {
        showTools = EditorPrefs.GetBool ("ShowTools", false);
        copyPosition = EditorPrefs.GetBool ("Copy Position", true);
        copyRotation = EditorPrefs.GetBool ("Copy Rotation", true);
        copyScale = EditorPrefs.GetBool ("Copy Scale", true);
        pastePosition = EditorPrefs.GetBool ("Paste Position", true);
        pasteRotation = EditorPrefs.GetBool ("Paste Rotation", true);
        pasteScale = EditorPrefs.GetBool ("Paste Scale", true);
    }
    
    void TransformCopyAll () {
        copyPosition = true;
        copyRotation = true;
        copyScale = true;
        GUI.changed = true;
    }
    
    void TransformCopyNone () {
        copyPosition = false;
        copyRotation = false;
        copyScale = false;
        GUI.changed = true;
    }
    
    void SetCopyPasteBools () {
        pastePosition = copyPosition;
        pasteRotation = copyRotation;
        pasteScale = copyScale;
        
        EditorPrefs.SetBool ("Copy Position", copyPosition);
        EditorPrefs.SetBool ("Copy Rotation", copyRotation);
        EditorPrefs.SetBool ("Copy Scale", copyScale);
        EditorPrefs.SetBool ("Paste Position", pastePosition);
        EditorPrefs.SetBool ("Paste Rotation", pasteRotation);
        EditorPrefs.SetBool ("Paste Scale", pasteScale);
    }
}

Align With Ground

Code snippet

using UnityEngine;
using UnityEditor;
using System.Collections;
    
public class AlignWithGround : MonoBehaviour {
    [MenuItem ("Tools/Transform Tools/Align with ground %t")]
    static void Align() {
        Transform [] transforms = Selection.transforms;
        foreach (Transform myTransform in transforms) {
            RaycastHit hit;
            if (Physics.Raycast (myTransform.position, -Vector3.up, out hit)) {
                Vector3 targetPosition = hit.point;
                if (myTransform.gameObject.GetComponent<MeshFilter>() != null) {
                    Bounds bounds = myTransform.gameObject.GetComponent<MeshFilter>().sharedMesh.bounds;
                    targetPosition.y += bounds.extents.y;
                }
                myTransform.position = targetPosition;
                Vector3 targetRotation = new Vector3 (hit.normal.x, myTransform.eulerAngles.y, hit.normal.z);
                myTransform.eulerAngles = targetRotation;
            }
        }
    }
}

My Player

Code snippet

using UnityEngine;
using System.Collections;

public class MyPlayer : MonoBehaviour {
    
    public int armor;

    public int damage;
    public GameObject gun;

    // Use this for initialization
    void Start () {
    
    }
    
    // Update is called once per frame
    void Update () {
    
    }
}

My Player Editor

Code snippet

using UnityEngine;
using UnityEditor;
using System.Collections;

[CustomEditor (typeof (MyPlayer))]
[CanEditMultipleObjects]
public class MyPlayerEditor : Editor {

    SerializedProperty damageProp;
    SerializedProperty armorProp;
    SerializedProperty gunProp;

    
    void OnEnable () {
        // Setup the SerializedProperties
        damageProp = serializedObject.FindProperty ("damage");
        armorProp = serializedObject.FindProperty ("armor");
        gunProp = serializedObject.FindProperty ("gun");
    }

    public override void OnInspectorGUI() {
        // Update the serializedProperty - always do this in the beginning of OnInspectorGUI.
        serializedObject.Update ();
        // Show the custom GUI controls
        EditorGUILayout.IntSlider (damageProp, 0, 100, new GUIContent ("Damage"));
        // Only show the damage progress bar if all the objects have the same damage value:
        if (!damageProp.hasMultipleDifferentValues)
            ProgressBar (damageProp.intValue / 100.0f, "Damage");
        EditorGUILayout.IntSlider (armorProp, 0, 100, new GUIContent ("Armor"));
        // Only show the armor progress bar if all the objects have the same armor value:
        if (!armorProp.hasMultipleDifferentValues)
            ProgressBar (armorProp.intValue / 100.0f, "Armor");
        EditorGUILayout.PropertyField (gunProp, new GUIContent ("Gun Object"));
        // Apply changes to the serializedProperty - always do this in the end of OnInspectorGUI.
        serializedObject.ApplyModifiedProperties ();
    }

    // Custom GUILayout progress bar.
    void ProgressBar (float value, string label) {
        // Get a rect for the progress bar using the same margins as a textfield:
        Rect rect = GUILayoutUtility.GetRect (18, 18, "TextField");
        EditorGUI.ProgressBar (rect, value, label);
        EditorGUILayout.Space ();
    }
}

Make UI Image

Code snippet

using UnityEngine;
using UnityEditor;

class MakeUIImage : AssetPostprocessor {
    void OnPreprocessTexture () {
        // Automatically convert any texture file with "GUIImages" in its file name into an uncompressed unchanged GUI Image.
        if (assetPath.Contains("UI_Images") || assetPath.Contains("SpriteFonts") || assetPath.Contains("SpriteAtlases")) {
            Debug.Log ("Importing new GUI Image!");
            TextureImporter myTextureImporter  = (TextureImporter)assetImporter;
            myTextureImporter.textureType = TextureImporterType.Advanced;
            myTextureImporter.textureFormat = TextureImporterFormat.ARGB32;
            myTextureImporter.convertToNormalmap = false;
            myTextureImporter.maxTextureSize = 2048;
            myTextureImporter.grayscaleToAlpha = false;
            myTextureImporter.generateCubemap = TextureImporterGenerateCubemap.None;
            myTextureImporter.npotScale = TextureImporterNPOTScale.None;
            myTextureImporter.isReadable = true;
            myTextureImporter.mipmapEnabled = false;
//            myTextureImporter.borderMipmap = false;
//            myTextureImporter.correctGamma = false;
            myTextureImporter.mipmapFilter = TextureImporterMipFilter.BoxFilter;
            myTextureImporter.fadeout = false;
//            myTextureImporter.mipmapFadeDistanceStart;
//            myTextureImporter.mipmapFadeDistanceEnd;
            myTextureImporter.convertToNormalmap = false;
//            myTextureImporter.normalmap;
//            myTextureImporter.normalmapFilter;
//            myTextureImporter.heightmapScale;
            myTextureImporter.lightmap = false;
            myTextureImporter.ClearPlatformTextureSettings("Web");
            myTextureImporter.ClearPlatformTextureSettings("Standalone");
            myTextureImporter.ClearPlatformTextureSettings("iPhone");
        }
    }
}

Scaled Curve

Code snippet

using UnityEngine;
using System.Collections;

// Custom serializable class
[System.Serializable]
public class ScaledCurve {
    public float scale = 1;
    public AnimationCurve curve = AnimationCurve.Linear (0, 0, 1, 1);
}

Testing Curves

Code snippet

using UnityEngine;
using System.Collections;

public class TestingCurves : MonoBehaviour {
    public ScaledCurve scaledCurve01;
    public ScaledCurve scaledCurve02;


    // Use this for initialization
    void Start () {
    
    }
    
    // Update is called once per frame
    void Update () {
    
    }
}

Another Testing Script

Code snippet

using UnityEngine;
using System.Collections;

public class AnotherTestingScript : MonoBehaviour {
    
    public ScaledCurve myScaledCurve;
    public int myInt;
    public Vector3 myV3;
    // Use this for initialization
    void Start () {
    
    }
    
    // Update is called once per frame
    void Update () {
    
    }
}

Scaled Curve Drawer

Code snippet

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer (typeof (ScaledCurve))]
public class ScaledCurveDrawer : PropertyDrawer {
    const int curveWidth = 50;
    const float min = 0;
    const float max = 1;
    public override void OnGUI (Rect pos, SerializedProperty prop, GUIContent label) {
        SerializedProperty scale = prop.FindPropertyRelative ("scale");
        SerializedProperty curve = prop.FindPropertyRelative ("curve");
        
        // Draw scale
        EditorGUI.Slider (
            new Rect (pos.x, pos.y, pos.width - curveWidth, pos.height),
            scale, min, max, label);
        
        // Draw curve
        int indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
        EditorGUI.PropertyField (
            new Rect (pos.width - curveWidth, pos.y, curveWidth, pos.height),
            curve, GUIContent.none);
        EditorGUI.indentLevel = indent;
    }
}

Regex Attribute

Code snippet

using UnityEngine;

public class RegexAttribute : PropertyAttribute {

    public readonly string pattern;
    public readonly string helpMessage;

    public RegexAttribute (string pattern, string helpMessage) {
        this.pattern = pattern;
        this.helpMessage = helpMessage;
    }
}

Regex Example

Code snippet

using UnityEngine;

public class RegexExample : MonoBehaviour {
    public string name;
    public GameObject tankModel;

    [Regex (@"^(?:\d{1,3}\.){3}\d{1,3}$", "Invalid IP address!\nExample: '127.0.0.1'")]
    public string serverAddress = "192.168.0.1";

}

Regex Drawer

Code snippet

using UnityEditor;
using UnityEngine;
using System.Text.RegularExpressions;

[CustomPropertyDrawer (typeof (RegexAttribute))]
public class RegexDrawer : PropertyDrawer {
    // These constants describe the height of the help box and the text field.
    const int helpHeight = 30;
    const int textHeight = 16;
    
    // Provide easy access to the RegexAttribute for reading information from it.
    RegexAttribute regexAttribute { get { return ((RegexAttribute)attribute); } }
    
    // Here you must define the height of your property drawer. Called by Unity.
    public override float GetPropertyHeight (SerializedProperty prop,
                                             GUIContent label) {
        if (IsValid (prop))
            return base.GetPropertyHeight (prop, label);
        else
            return base.GetPropertyHeight (prop, label) + helpHeight;
    }
    
    // Here you can define the GUI for your property drawer. Called by Unity.
    public override void OnGUI (Rect position, SerializedProperty prop, GUIContent label) {
        // Adjust height of the text field
        Rect textFieldPosition = position;
        textFieldPosition.height = textHeight;
        DrawTextField (textFieldPosition, prop, label);
        
        // Adjust the help box position to appear indented underneath the text field.
        Rect helpPosition = EditorGUI.IndentedRect (position);
        helpPosition.y += textHeight;
        helpPosition.height = helpHeight;
        DrawHelpBox (helpPosition, prop);
    }
    
    void DrawTextField (Rect position, SerializedProperty prop, GUIContent label) {
        // Draw the text field control GUI.
        EditorGUI.BeginChangeCheck ();
        string value = EditorGUI.TextField (position, label, prop.stringValue);
        if (EditorGUI.EndChangeCheck ())
            prop.stringValue = value;
    }
    
    void DrawHelpBox (Rect position, SerializedProperty prop) {
        // No need for a help box if the pattern is valid.
        if (IsValid (prop))
            return;
        
        EditorGUI.HelpBox (position, regexAttribute.helpMessage, MessageType.Error);
    }
    
    // Test if the propertys string value matches the regex pattern.
    bool IsValid (SerializedProperty prop) {
        return Regex.IsMatch (prop.stringValue, regexAttribute.pattern);
    }
}

Ingredient Testing

Code snippet

using UnityEngine;

public enum IngredientUnit { Spoon, Cup, Bowl, Piece }

// Custom serializable class
[System.Serializable]
public class Ingredient{
    public string name;
    public int amount = 1;
    public IngredientUnit unit;
}

//This is not an editor script
class IngredientTesting : MonoBehaviour{
    [Range (1,10)]
    public int myInt;
    public Ingredient potionResult;
    public Ingredient[] potionIngredients;
    
    void Update () {
        // Update logic here...
    }
}

Ingredient Drawer

Code snippet

using UnityEngine;
using UnityEditor;
using System.Collections;


[CustomPropertyDrawer (typeof (Ingredient))]
class IngredientDrawer : PropertyDrawer {
    
    // Draw the property inside the given rect
    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
        // Using BeginProperty / EndProperty on the parent property means that
        // prefab override logic works on the entire property.
        EditorGUI.BeginProperty (position, label, property);
        
        // Draw label
        position = EditorGUI.PrefixLabel (position, GUIUtility.GetControlID (FocusType.Passive), label);
        
        // Don't make child fields be indented
        var indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
        
        // Calculate rects
        Rect amountRect = new Rect (position.x, position.y, 30, position.height);
        Rect unitRect = new Rect (position.x + 35, position.y, 50, position.height);
        Rect nameRect = new Rect (position.x + 90, position.y, position.width - 90, position.height);
        
        // Draw fields - passs GUIContent.none to each so they are drawn without labels
        EditorGUI.PropertyField (amountRect, property.FindPropertyRelative ("amount"), GUIContent.none);
        EditorGUI.PropertyField (unitRect, property.FindPropertyRelative ("unit"), GUIContent.none);
        EditorGUI.PropertyField (nameRect, property.FindPropertyRelative ("name"), GUIContent.none);
        
        // Set indent back to what it was
        EditorGUI.indentLevel = indent;
        
        EditorGUI.EndProperty ();
    }
}

Scripting

  1. Scripts as Behaviour Components
  2. Variables and Functions
  3. Conventions and Syntax
  4. C# vs JS syntax
  5. IF Statements
  6. Loops
  7. Scope and Access Modifiers
  8. Awake and Start
  9. Update and FixedUpdate
  10. Vector Maths
  11. Enabling and Disabling Components
  12. Activating GameObjects
  13. Translate and Rotate
  14. Look At
  15. Linear Interpolation
  16. Destroy
  17. GetButton and GetKey
  18. GetAxis
  19. OnMouseDown
  20. GetComponent
  21. Delta Time
  22. Data Types
  23. Classes
  24. Instantiate
  25. Arrays
  26. Invoke
  27. Enumerations
  28. Switch Statements
  1. Properties
  2. Ternary Operator
  3. Statics
  4. Method Overloading
  5. Generics
  6. Inheritance
  7. Polymorphism
  8. Member Hiding
  9. Overriding
  10. Interfaces
  11. Extension Methods
  12. Namespaces
  13. Lists and Dictionaries
  14. Coroutines
  15. Quaternions
  16. Delegates
  17. Attributes
  18. Events
  1. Building a Custom Inspector
  2. The DrawDefaultInspector Function
  3. Adding Buttons to a Custom Inspector
  1. MonoDevelop's Debugger
  2. Good Coding Practices in Unity
  3. Unity Editor Extensions – Menu Items
  4. Creating Meshes
  1. AssetBundles and the AssetBundle Manager
  2. Mastering Unity Project Folder Structure - Version Control Systems
  1. Installing Tools for Unity Development
  2. Building your first Unity Game with Visual Studio
  3. Editing Unity games in Visual Studio
  4. Debugging Unity games in Visual Studio
  5. Graphics debugging Unity games in Visual Studio
  6. Taking Unity games to Universal Windows Platform
  7. Testing Unity games on Android in Visual Studio
  1. Scripting Primer and Q&A
  2. Scripting Primer and Q&A - Continued
  3. Scripting Primer and Q&A - Continued (Again)
  4. Persistence - Saving and Loading Data
  5. Object Pooling
  6. Introduction to Scriptable Objects
  7. How to communicate between Scripts and GameObjects
  8. Coding in Unity for the Absolute Beginner
  9. Sound Effects & Scripting
  10. Editor Scripting Intro
  11. Writing Plugins
  12. Property Drawers & Custom Inspectors
  13. Events: Creating a simple messaging system
  14. Ability System with Scriptable Objects
  15. Character Select System with Scriptable Objects
  1. Intro and Setup
  2. Data Classes
  3. Menu Screen
  4. Game UI
  5. Answer Button
  6. Displaying Questions
  7. Click To Answer
  8. Ending The Game and Q&A
  1. Intro To Part Two
  2. High Score with PlayerPrefs
  3. Serialization and Game Data
  4. Loading Game Data via JSON
  5. Loading and Saving via Editor Script
  6. Game Data Editor GUI
  7. Question and Answer
  1. Overview and Goals
  2. Localization Data
  3. Dictionary, JSON and Streaming Assets
  4. Localization Manager
  5. Startup Manager
  6. Localized Text Component
  7. Localized Text Editor Script
  8. Localization Q&A
  1. Introduction and Session Goals
  2. Particle Launcher
  3. Particle Collisions
  4. ParticleLauncher Script
  5. Particle Collisions and Scripting
  6. Random Particle Colors
  7. Drawing Decals with Particles
  8. Collecting Particle Information For Display
  9. Displaying Particles Via Script
  10. Droplet Decals
  11. Questions and Answers