Search Unity

How to best extend builtin component functionality in the editor

Discussion in 'Immediate Mode GUI (IMGUI)' started by Xarbrough, Jan 4, 2017.

  1. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    This is more of a general question with a specific example: What are good ways to extend Unity builtin components such as Collider or LineRenderer with custom functionality in regards to custom editor code?

    Of course at runtime, I mostly only have a choice of composing my own MonoBehaviour with a reference to said Unity components and then add my own fields and methods there. So now I can write a custom editor for my custom type and provide some additional editor only functionality.

    Example:
    I have a LineRenderer, which should always "loop" like a circle shape; this means the first and last point of the positions array need to be the same. Also I would like scene view Handles to drag each LineRenderer point in the scene.

    I already have my desired features coded and working, but there are a few inconveniences:
    - Since I don't have access to the LineRenderers editor, I need to create my own SerializedObject from a GetComponent reference and cache all SerializedProperties which I'm interested in myself.
    - I don't get any changed events like GUI.changed, BeginChangeCheck, OnValidate or Reset.
    - Overall I have to write a lot of boilerplate code just to get information, which the editor already knows about, but doesn't expose.

    Is there any better approach to this sort of problem? I can post my code as an example, but I'm also interested in general advice.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. #if UNITY_EDITOR
    5. using UnityEditor;
    6. #endif
    7.  
    8. [RequireComponent(typeof(LineRenderer))]
    9. public class LoopLineRenderer : MonoBehaviour
    10. {
    11.     void OnEnable()
    12.     {
    13.         // This class is empty because we only need it as a hook for our custom inspector.
    14.         // We define OnEnable, because we can use the toggle to temporily disable the loop behaviour.
    15.     }
    16. }
    17.  
    18. #if UNITY_EDITOR
    19. [CustomEditor(typeof(LoopLineRenderer))]
    20. public class LoopLineRendererEditor : Editor
    21. {
    22.     Vector3 previousFirst;
    23.     Vector3 previousLast;
    24.     LoopLineRenderer script;
    25.     SerializedObject lineRendererSO;
    26.     SerializedProperty positionsProp;
    27.     System.Func<Vector3, Vector3> handleTransformation;
    28.     System.Func<Vector3, Vector3> handleInverseTransformation;
    29.  
    30.     void OnEnable()
    31.     {
    32.         script = (LoopLineRenderer)target;
    33.  
    34.         LineRenderer lineRenderer = script.GetComponent<LineRenderer>();
    35.         if(lineRenderer.useWorldSpace)
    36.             handleTransformation = v => v;
    37.         else
    38.             handleTransformation = v => lineRenderer.transform.TransformPoint(v);
    39.        
    40.         if(lineRenderer.useWorldSpace)
    41.             handleInverseTransformation = v => v;
    42.         else
    43.             handleInverseTransformation = v => lineRenderer.transform.InverseTransformPoint(v);
    44.  
    45.         lineRendererSO = new SerializedObject(lineRenderer);
    46.         positionsProp = lineRendererSO.FindProperty("m_Positions");
    47.     }
    48.  
    49.     public override void OnInspectorGUI ()
    50.     {
    51.         base.OnInspectorGUI();
    52.  
    53.         EditorGUILayout.HelpBox("When enabled and folded out, this component ensures, that the first and last position vector of an attached LineRenderer match.", MessageType.Info);
    54.  
    55.         if(positionsProp.arraySize < 3)
    56.             script.enabled = false;
    57.  
    58.         if(script.enabled && positionsProp.arraySize > 2)
    59.         {
    60.             lineRendererSO.Update();
    61.             var first = positionsProp.GetArrayElementAtIndex(0);
    62.             var last = positionsProp.GetArrayElementAtIndex(positionsProp.arraySize - 1);
    63.  
    64.             if(first.vector3Value != previousFirst)
    65.                 last.vector3Value = first.vector3Value;
    66.            
    67.             else if(last.vector3Value != previousLast)
    68.                 first.vector3Value = last.vector3Value;
    69.  
    70.             lineRendererSO.ApplyModifiedProperties();
    71.  
    72.             previousFirst = first.vector3Value;
    73.             previousLast = last.vector3Value;
    74.         }
    75.     }
    76.  
    77.     void OnSceneGUI()
    78.     {
    79.         if(!script.enabled)
    80.             return;
    81.        
    82.         var first = positionsProp.GetArrayElementAtIndex(0);
    83.         var last = positionsProp.GetArrayElementAtIndex(positionsProp.arraySize - 1);
    84.         drawHandleForProps(script.transform, first, last);
    85.  
    86.         for (int i = 1; i < positionsProp.arraySize - 1; i++)
    87.             drawHandleForProp(script.transform, positionsProp.GetArrayElementAtIndex(i));
    88.  
    89.         lineRendererSO.ApplyModifiedProperties();
    90.     }
    91.  
    92.     void drawHandleForProp(Transform transform, SerializedProperty positionProp)
    93.     {
    94.         EditorGUI.BeginChangeCheck();
    95.         Vector3 newValue = Handles.PositionHandle(handleTransformation(positionProp.vector3Value), transform.rotation);
    96.         if(EditorGUI.EndChangeCheck())
    97.         {
    98.             positionProp.vector3Value = handleInverseTransformation(newValue);
    99.         }
    100.     }
    101.  
    102.     void drawHandleForProps(Transform transform, SerializedProperty positionPropA, SerializedProperty positionPropB)
    103.     {
    104.         EditorGUI.BeginChangeCheck();
    105.         Vector3 newValue = Handles.PositionHandle(handleTransformation(positionPropA.vector3Value), transform.rotation);
    106.         if(EditorGUI.EndChangeCheck())
    107.         {
    108.             positionPropA.vector3Value = handleInverseTransformation(newValue);
    109.             positionPropB.vector3Value = handleInverseTransformation(newValue);
    110.         }
    111.     }
    112. }
    113. #endif
    114.  
     
  2. Deleted User

    Deleted User

    Guest

    Somehow all editor extension code eventually ends up looking something like this. People more acquainted with the API may do some fancy tricks by retrieving information using reflection, and people with an enterprise deal will just get the source code and change what needs changing. There is not much you can do in terms of clever OOP. Some classes here and there to wrap some functionality, but still ugly on the lowest level. For example, to check whether an orthographic SceneView is currently viewing from one of the main axes, I need to check the camera transform's forward vector. However, there are rounding errors, so I need to do some kind of AlmostEqual() with some small threshold value. It's ugly as hell, but it gets the job done. I would not worry about the code too much. It always gets messy as soon as you want to do just slightly more advanced stuff. Just make sure it actually works, is what I say anyway.
     
    PanoTron and Xarbrough like this.
  3. PsyKaw

    PsyKaw

    Joined:
    Aug 16, 2012
    Posts:
    102
    I think it's better to do :

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEditor;
    4.  
    5. [CustomEditor(typeof(LineRenderer))]
    6. public class LineRendererEditor : Editor
    7. {
    8.     private SerializedProperty m_positionProperty;
    9.     private bool m_loop = false;
    10.     private GUIContent m_loopContent = new GUIContent("Looping");
    11.  
    12.     protected LineRenderer target
    13.     {
    14.         get { return base.target as LineRenderer; }
    15.     }
    16.  
    17.     public void OnEnable()
    18.     {
    19.         m_positionProperty = serializedObject.FindProperty("m_Positions");
    20.         if (m_positionProperty.arraySize > 1)
    21.         {
    22.             m_loop = m_positionProperty.GetArrayElementAtIndex(0).vector3Value == m_positionProperty.GetArrayElementAtIndex(m_positionProperty.arraySize - 1).vector3Value;
    23.         }
    24.     }
    25.  
    26.     public override void OnInspectorGUI()
    27.     {
    28.         serializedObject.Update();
    29.      
    30.         EditorGUI.BeginChangeCheck();
    31.  
    32.         DrawDefaultInspector();
    33.  
    34.         m_loop = EditorGUILayout.Toggle(m_loopContent, m_loop);
    35.         if (EditorGUI.EndChangeCheck())
    36.         {
    37.             CheckForceLoop();
    38.         }
    39.  
    40.         serializedObject.ApplyModifiedProperties();
    41.     }
    42.  
    43.     void OnSceneGUI()
    44.     {
    45.         serializedObject.Update();
    46.  
    47.         EditorGUI.BeginChangeCheck();
    48.         for (int i = 0; i < (m_loop?m_positionProperty.arraySize-1:m_positionProperty.arraySize); i++)
    49.             DrawHandleForProp(target.transform, m_positionProperty.GetArrayElementAtIndex(i));
    50.         if (EditorGUI.EndChangeCheck())
    51.         {
    52.             CheckForceLoop();
    53.         }
    54.         serializedObject.ApplyModifiedProperties();
    55.     }
    56.  
    57.     void DrawHandleForProp(Transform transform, SerializedProperty positionProp)
    58.     {
    59.         EditorGUI.BeginChangeCheck();
    60.         Vector3 position = positionProp.vector3Value;
    61.         if (!target.useWorldSpace)
    62.             position = target.transform.TransformPoint(position);
    63.  
    64.         position = Handles.PositionHandle(position, transform.rotation);
    65.         if (EditorGUI.EndChangeCheck())
    66.         {
    67.             if (!target.useWorldSpace)
    68.                 position = target.transform.InverseTransformPoint(position);
    69.             positionProp.vector3Value = position;
    70.         }
    71.     }
    72.  
    73.     void CheckForceLoop()
    74.     {
    75.         if (m_loop && m_positionProperty.arraySize > 1)
    76.         {
    77.             m_positionProperty.GetArrayElementAtIndex(m_positionProperty.arraySize - 1).vector3Value = m_positionProperty.GetArrayElementAtIndex(0).vector3Value;
    78.         }
    79.     }
    80. }
    81.  
    82.  
    You can make a custom editor for a built-in type, so no more useless component LoopLineRenderer and no need to create a new SerializedObject.

    Enjoy!
     
    Last edited: Jun 23, 2017
  4. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    Thanks for this suggestion! Some ideas I like, however, one issue with this approach is, that I might not want all my LineRenderers to have this functionality. At least with some other more specialized things, it could be an issue. With this, it would probably be ok.
     
  5. Crystalline

    Crystalline

    Joined:
    Sep 11, 2013
    Posts:
    171

    Great piece of code. But there is no such thing as "base editor", instead just "Editor"
     
  6. PsyKaw

    PsyKaw

    Joined:
    Aug 16, 2012
    Posts:
    102
    Whoops, sorry. Forgot to replace my custom class, I edited my post, thanks.
     
  7. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    Just wanted to share, that it works pretty well to extend Unity components in the following pattern:

    - Override Editor for builtin component
    - Check for specific conditions like "Use custom inspector only when attached renderer also has a certain material"
    - Search UnityEditor assembly for specific builtin inspector e.g. SpriteRendererEditor and instantiate
    - Use the instantiated editor within custom editor to draw default implementation and optionally adding custom functionality depending on condition

    This of course only applies for editors, which are not exposed and have a custom implementation made by Unity.
     
    kodo91 likes this.