Property Drawers & Custom Inspectors

確認済のバージョン: 4.6

-

難易度: 中級

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

中級 Interface & Essentials

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() != null) {
                    Bounds bounds = myTransform.gameObject.GetComponent().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 ();
    }
}

関連するチュートリアル