monty
monty

Reputation: 8745

Unity3D custom inspector not saving values correctly

I try to achieve a tiny custom editor in Unity3D which offers buttons to modify a value in two components of the same game object. So in the "PlanetGameObject" there is the MonoBehaviour "Planet" where the member "phase" should change (to one of the matching enum values) and also a SpriteRenderer where the member "color" changes to the matching value of the phase.

Basically it works as intended. When I click the buttons in the editor both values get assigned correctly but when the game starts the phase switches back to the initial value while the color of the sprite renderer stays with the new value. When I close the running game the incorrect value stays.

The setup:

enter image description here

Planet script:

using UnityEngine;

public class Planet : MonoBehaviour
{

    public enum Phase { NEUTRAL, RED, GREEN, BLUE }
    
    public Phase phase = Phase.NEUTRAL;

    public static Color PhaseColor(Phase phase)
    {
        switch (phase)
        {
            case Phase.RED: return Color.red;
            case Phase.GREEN: return Color.green;
            case Phase.BLUE: return Color.blue;
            default: return Color.white;
        }
    }
   
}

The custom editor script:

[![using UnityEngine;
using UnityEditor;

\[CustomEditor(typeof(Planet))\]
\[CanEditMultipleObjects\]
public class PlanetEditor : Editor
{

    public override void OnInspectorGUI()
    {

        GUILayout.BeginHorizontal();

        if (GUILayout.Button("neutral"))
        {
            foreach(Object t in targets)
            {
                Planet planet = (Planet)t;
                planet.phase = Planet.Phase.NEUTRAL;
                planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
            }
        };

        if (GUILayout.Button("red"))
        {
            foreach (Object t in targets)
            {
                Planet planet = (Planet)t;
                planet.phase = Planet.Phase.RED;
                planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
            }
        };

        if (GUILayout.Button("green"))
        {
            foreach (Object t in targets)
            {
                Planet planet = (Planet)t;
                planet.phase = Planet.Phase.GREEN;
                planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
            }
        };

        if (GUILayout.Button("blue"))
        {
            foreach (Object t in targets)
            {
                Planet planet = (Planet)t;
                planet.phase = Planet.Phase.BLUE;
                planet.gameObject.GetComponent<SpriteRenderer>().color = Planet.PhaseColor(planet.phase);
            }
        };

        GUILayout.EndHorizontal();

        DrawDefaultInspector();

    }

}][1]][1]

Upvotes: 0

Views: 3854

Answers (1)

derHugo
derHugo

Reputation: 90610

Never go through the actual component references in Editor scripts.

This won't work with saving and undo/redo etc. because it doesn't mark the changes as dirty automatically.

Rather go through the SerializedPropertys which automatically handles marking stuff as dirty => saving. And Undo/Redo etc.

Could look somewhat like

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(Planet))]
[CanEditMultipleObjects]
public class PlanetEditor : Editor
{

    SerializedProperty _phase;

    private void OnEnable()
    {
        // This links the _phase SerializedProperty to the according actual field
        _phase = serializedObject.FindProperty("phase");
    }

    public override void OnInspectorGUI()
    {
        // This gets the current values from all serialized fields into the serialized "clone"
        serilaizedObject.Update();

        GUILayout.BeginHorizontal();

        if (GUILayout.Button("neutral"))
        {
            // It is enough to do this without a loop as this will already affect all selected instances
            phase.intValue = (int)Planet.Phase.NEUTRAL;
        };

        if (GUILayout.Button("red"))
        { 
            phase.intValue = (int)Planet.Phase.RED;
        };

        if (GUILayout.Button("green"))
        {
            phase.intValue = (int)Planet.Phase.GREEN;
        };

        if (GUILayout.Button("blue"))
        {
            phase.intValue = (int)Planet.Phase.RED;
        };

        GUILayout.EndHorizontal();

        // This writes the changed properties back into the actual instance(s)
        serializedObject.ApplyModifiedProperties();

        DrawDefaultInspector();
    }
}

So far for saving the enum of this component itself.

Now the simpliest way for the renderer would be to let your class itself react to that change:

[RequireComponent(typeof(SpriteRenderer))]
public class Planet : MonoBehaviour
{
    public enum Phase { NEUTRAL, RED, GREEN, BLUE }
    
    public Phase phase = Phase.NEUTRAL;

    // Will be called everytime a field in the Inspector is changed
    private void OnValidate()
    {
        GetComponent<SpriteRenderer>().color = PhaseColor(phase);
    }

    public static Color PhaseColor(Phase phase)
    {
        switch (phase)
        {
            case Phase.RED: return Color.red;
            case Phase.GREEN: return Color.green;
            case Phase.BLUE: return Color.blue;
            default: return Color.white;
        }
    } 
}

As this also should handle the marking dirty and thereby saving correctly afaik.

Actually your editor becomes a bit redundant with this since this also already reacts if you change the enum field manually ;)


Note: Typed on smartphone but I hope the idea gets clear

Upvotes: 3

Related Questions