Search code examples
c#unity-game-engineunity-editor

Unity Editor Mode - How do I get OnHierarchyChange to repaint the scene when children are added/deleted to the object the script is on?


Have script which runs in [ExecuteAlways] mode. It is mainly a function called at Start() and OnValidate() to update the position of objects based on changes in an Editor. This all works fine.

When an object is added as a child to the object with the script in the Hierarchy window I want UpdateRing() to be called and integrate that into the ring. Putting OnHierarchyChange() with UpdateRing() doesn't seem to do anything. In other questions OnHierarchyChange() is put in the Editor file but I don't know how I can put OnHierarchyChange() in the Editor file and call UpdateRing()...or if that is even something I should do...

GameObject Code:

using UnityEngine;
using System;
using System.ComponentModel;

[Serializable]
[ExecuteAlways]
public class ObjectsRing : MonoBehaviour
{
//public float radius = { get { return m_Radius; } set { m_Radius = value; } }
[Range(0f, 100f)]
public float radius = 10;

[Range(0f,360f)]
public float beginAngle = 0f;

[Range(0f,360f)]
public float endAngle = 360f;

public bool flip = false;

public enum orientationList {[Description("X-up")] Xup, [Description("Y-up")] Yup, [Description("Z-up")] Zup};

public orientationList orientation;    

// Start is called before the first frame update
void Start()
{
    UpdateRing();
}

// OnValidate is called when fields are changed in an Editor
void OnValidate()
{
    UpdateRing();       
}

// OnHierarchyChange is called when changes are made in the Hierarchy pane. 
void OnHierarchyChange()
{
    UpdateRing();  
}

private void UpdateRing()
{
    //Input error handling
    if (endAngle < beginAngle)
    {
        float tempAngle = beginAngle; 
        beginAngle = endAngle;
        endAngle = tempAngle;
    }

    // Attach mesh, rotate object and add material
    float objectAngle = (endAngle - beginAngle) / (transform.childCount);
    float rotation = beginAngle;
    for (int cnt = 0; cnt < transform.childCount; cnt++)
    {
        // Translate and rotate each object
        transform.GetChild(cnt).GetComponent<Transform>().localPosition = new Vector3(radius, 0, 0);
        // transform.GetChild(cnt).GetComponent<Transform>().rotation = Quaternion.Euler(0, rotation, 0);
        rotation = beginAngle + cnt * objectAngle;
        transform.GetChild(cnt).RotateAround(transform.position, new Vector3(0,1,0), rotation);
        transform.GetChild(cnt).LookAt(transform.position);
        if (flip)
            {
            transform.GetChild(cnt).Rotate(new Vector3(0,180,0));
        }
            switch (orientation)
            {
                case orientationList.Xup:
                {
                    transform.GetChild(cnt).Rotate(new Vector3(0,0,0));
                    break;                
                }         
                case orientationList.Yup:
                {
                    transform.GetChild(cnt).Rotate(new Vector3(90,0,0));
                    break;                
                }         
                case orientationList.Zup:
                {
                    transform.GetChild(cnt).Rotate(new Vector3(0,0,90));
                    break;                
                }                  
            }
        }
    }
}

Editor Code:

using UnityEditor;

[CustomEditor(typeof(ObjectsRing)), CanEditMultipleObjects]
public class ObjectsRingEditor : Editor
{
    private SerializedProperty radiusProperty;
    private SerializedProperty beginAngleProperty;
    private SerializedProperty endAngleProperty;
    private SerializedProperty flipProperty;
    private SerializedProperty orientationProperty;

    public void OnEnable()
    {
        radiusProperty = serializedObject.FindProperty("radius");
        beginAngleProperty = serializedObject.FindProperty("beginAngle");
        endAngleProperty = serializedObject.FindProperty("endAngle");
        flipProperty = serializedObject.FindProperty("flip");
        orientationProperty = serializedObject.FindProperty("orientation");
    }


    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        radiusProperty.floatValue = EditorGUILayout.Slider ("Radius", radiusProperty.floatValue, 0, 100);
        beginAngleProperty.floatValue = EditorGUILayout.Slider ("Begin Angle", beginAngleProperty.floatValue, 0, 360);
        endAngleProperty.floatValue = EditorGUILayout.Slider ("End Angle", endAngleProperty.floatValue, 0, 360);
        flipProperty.boolValue = EditorGUILayout.Toggle ("Flip", flipProperty.boolValue);
        orientationProperty.enumValueIndex = EditorGUILayout.Popup ("Orientation", orientationProperty.enumValueIndex, orientationProperty.enumDisplayNames);

        serializedObject.ApplyModifiedProperties();
        EditorApplication.update.Invoke();
    }
}

Solution

  • As already mentioned in other answers OnHierarchyChange is a message of EditorWindow and will only be called by Unity in this type of class, not in a MonoBehaviour.


    However, the solution is actually quite simple!

    If tagging your class [ExecuteAllways] or [ExecuteInEditMode] the method in MonoBehaviour classes getting called if anything in the Scene changes is simply Update!

    Update is only called when something in the Scene changed.

    Changing something in the hierarchy implies that also something in the scene is changed.

    So you can simply put it I Update and only have to prevent it from running later in the build application

    #if UNITY_EDITOR
        private void Update()
        {
            UpdateRing();
        }
    #endif
    

    the #if UNITY_EDIROR preprocessor will make sure this part is removed in a build avoiding overhead of Update getting called entirely.

    In case you need the Update for something else you could alternatively also either do

        private void Update()
        {
    #if UNITY_EDITOR
            UpdateRing();
    #endif
    
            ...
        }
    

    or also

    private void Update()
    {
        if(Application.isEditor)
        {
            UpdateRing();
        }
    
        ...
    }
    

    Sidenotes

    • Actually what is the custom Editor script for? I don't see it adding anything that wouldn't be drawn in the Inspector by default anyway ...

    • [Serializable] is redundant on a class of type MonoBehaviour.