Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Resolved Gradient field in the inspector does not update to reflect the change to it.

Discussion in 'Scripting' started by rayD8, Mar 12, 2023.

  1. rayD8

    rayD8

    Joined:
    Mar 7, 2018
    Posts:
    32
    I have a script ("GradientIssue_TEST") that has a Gradient field.
    It uses a ScriptableObject with an array of Gradients to select the gradient to use.
    When selecting a gradient from the list... everything works as expected...
    Except the Gradient field in the inspector does not update to reflect the change unless I click off of the object with the script (so the script not showing in the Inspector tab), and then click back on it.
    Despite the Repaint() method being used. (and called successfully - checked with a Debug.Log line)
    Code (CSharp):
    1. using UnityEngine;
    2. namespace Synesthesure
    3. {
    4.     public class GradientIssue_TEST : MonoBehaviour
    5.     {
    6.         [SerializeField] GradientLibrary gradients;
    7.         public int gradientToUse = -1;
    8.         [SerializeField] Gradient gradient; // <---- ISSUE IS HERE
    9.         int previousGradient;
    10.  
    11.         void Awake()
    12.         {
    13.             previousGradient = gradientToUse;
    14.             if (gradientToUse > -1 && gradients != null) SetGradient(gradientToUse);
    15.         }
    16.  
    17.         void OnValidate()
    18.         {
    19.             if (gradientToUse != previousGradient)
    20.             {
    21.                 previousGradient = gradientToUse;
    22.                 SetGradient(gradientToUse);
    23.             }
    24.         }
    25.  
    26.         public void SetGradient(int index)
    27.         {
    28.             if (gradients == null) return;
    29.             if (index > -1 && index < gradients.gradients.Length)
    30.             {
    31.                 gradient = gradients.gradients[index];
    32.                 previousGradient = index;
    33.             }
    34.         }
    35.     }
    36. }
    37.  
    Code (CSharp):
    1. using UnityEditor;
    2. namespace Synesthesure
    3. {
    4.     [CustomEditor(typeof(GradientIssue_TEST))]
    5.     public class GradientIssue_TEST_Editor : Editor
    6.     {
    7.         static bool showColorInfo = false;
    8.         public override void OnInspectorGUI()
    9.         {
    10.             serializedObject.Update();
    11.             showColorInfo = EditorGUILayout.Foldout(showColorInfo, "Color");
    12.             if (showColorInfo)
    13.             {
    14.                 EditorGUILayout.PropertyField(serializedObject.FindProperty("gradients"));
    15.                 EditorGUILayout.PropertyField(serializedObject.FindProperty("gradientToUse"));
    16.                 EditorGUILayout.PropertyField(serializedObject.FindProperty("gradient"));
    17.             }
    18.             serializedObject.ApplyModifiedProperties();
    19.             Repaint();  // <--- THIS DOES NOT REPAINT THE GRADIENT FIELD
    20.             //UnityEngine.Debug.Log("got this far...");
    21.         }
    22.     }
    23. }
    24.  
    Code (CSharp):
    1. using UnityEngine;
    2. namespace Synesthesure
    3. {
    4.     [CreateAssetMenu(fileName = "Gradient Library", menuName = "Synesthesure/Gradient Library")]
    5.     public class GradientLibrary : ScriptableObject
    6.     {
    7.         public Gradient[] gradients;
    8.     }
    9. }
    10.  
     
    Last edited: Mar 12, 2023
  2. samana1407

    samana1407

    Joined:
    Aug 23, 2015
    Posts:
    74
    I think first you need to ensure proper behavior within the class itself, forgetting about OnValidate and the custom inspector. After all, there will be no inspector or OnValidate in the game.

    And after that, create an inspector and the necessary behavior inside it.

    Perhaps this is not the best solution, but try adding properties for this.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. namespace Synesthesure
    4. {
    5.  
    6.     public class GradientIssue_TEST : MonoBehaviour
    7.     {
    8.         [SerializeField] GradientLibrary _gradientsLib;
    9.  
    10.         [SerializeField] private int _prevGradientIndex;
    11.         [SerializeField] private int _gradientToUse;
    12.  
    13.         [SerializeField] Gradient _gradient;
    14.  
    15.         public GradientLibrary GradientLib
    16.         {
    17.             get => _gradientsLib;
    18.             set => SetGradientLib(value);
    19.         }
    20.         public int GradientToUse
    21.         {
    22.             get => _gradientToUse;
    23.             set => SetGradientIndex(value);
    24.         }
    25.  
    26.  
    27.         private void SetGradientIndex(int index)
    28.         {
    29.             if (_gradientsLib == null)
    30.                 return;
    31.  
    32.             if (index == _prevGradientIndex)
    33.                 return;
    34.  
    35.             _prevGradientIndex = _gradientToUse;
    36.  
    37.             _gradientToUse = Mathf.Clamp(index, 0, _gradientsLib.gradients.Length - 1);
    38.  
    39.             _gradient = _gradientsLib.gradients[_gradientToUse];
    40.         }
    41.  
    42.         private void SetGradientLib(GradientLibrary value)
    43.         {
    44.             if (value == null)
    45.             {
    46.                 _gradientsLib = null;
    47.                 return;
    48.             }
    49.  
    50.  
    51.             if (value != _gradientsLib)
    52.             {
    53.                 _gradientsLib = value;
    54.  
    55.                 _prevGradientIndex = -1;
    56.                 SetGradientIndex(0);
    57.             }
    58.  
    59.         }
    60.      
    61.     }
    62. }
    ----------------------------------------------------------

    Inspector

    Code (CSharp):
    1. using UnityEditor;
    2.  
    3. namespace Synesthesure
    4. {
    5.  
    6.     [CustomEditor(typeof(GradientIssue_TEST))]
    7.     public class GradientIssue_TEST_Editor : Editor
    8.     {
    9.         static bool showColorInfo = false;
    10.         private GradientIssue_TEST _target;
    11.  
    12.         private void OnEnable()
    13.         {
    14.             _target = (GradientIssue_TEST)target;
    15.         }
    16.         public override void OnInspectorGUI()
    17.         {
    18.             serializedObject.Update();
    19.  
    20.             showColorInfo = EditorGUILayout.Foldout(showColorInfo, "Color");
    21.             if (showColorInfo)
    22.             {
    23.                 EditorGUI.BeginChangeCheck();
    24.  
    25.                 _target.GradientLib = (GradientLibrary)EditorGUILayout.ObjectField("Lib", _target.GradientLib, typeof(GradientLibrary), true);
    26.                 _target.GradientToUse = EditorGUILayout.IntField("GradientToUse", _target.GradientToUse);
    27.  
    28.                 if (EditorGUI.EndChangeCheck())
    29.                 {
    30.                     EditorUtility.SetDirty(_target);
    31.                 }
    32.  
    33.                 EditorGUILayout.PropertyField(serializedObject.FindProperty("_gradient"));
    34.             }
    35.  
    36.             serializedObject.ApplyModifiedProperties();
    37.         }
    38.     }
    39. }
    40.  
     
  3. rayD8

    rayD8

    Joined:
    Mar 7, 2018
    Posts:
    32
    >> After all, there will be no inspector or OnValidate in the game.
    That's what the public void SetGradient(int index) method is for...

    Everything works fine and as expected "in game" / Play time / Scene window.
    GradientIssue_TEST and GradientIssue_TEST_Editor classes are dumbed-down. The actual classes blends two gradients into a new gradient and have a lot of details not relevant to replicating the issue.

    The issue is about what's displayed on the editor inspector tab during the content creation process when editing.

    Which requires the OnValidate method.

    When making a change to the field "gradientToUse" in the editor to select a gradient from the ScriptableObject (GradientLibrary) that holds a "library" of gradients to choose from...
    The associated field "gradient" does not repaint/refresh its displayed gradient with the colors of the gradient that was selected by changing the value of "gradientToUse".
    HOWEVER, the change *IS* happening in the Play window when the field "gradientToUse" is changed.
    Everything works fine and as expected "in game" / Play time / Scene window.
    The gradient displayed on the inspector tab does not get refreshed/repainted with the correct colors without deselecting its gameObject and then re-selecting it. Regardless of whether it's in Play mode or not.
     
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    Well, the issue is not what is displayed. The issue is that you're not correctly editing serialized objects inside the Unity editor. You're just forcefully changing the values behind the scenes without communicating your changes to the editor. Therefore those changes are not serialized. They could accidentally be serialized when another proper change happens which causes the object to be serialized.

    You don't really should need the OnValidate method at all when you have a custom editor / PropertyDrawer. Your usage of gradientToUse and previousGradient is hacky at best. Though it could work when you do something like this:

    Code (CSharp):
    1. public void SetGradient(int index)
    2.         {
    3.             if (gradients == null) return;
    4.             if (index > -1 && index < gradients.gradients.Length)
    5.             {
    6.                 #if UNITY_EDITOR
    7.                 UnityEditor.Undo.RegisterObject(this);
    8.                 #endif
    9.                 gradient = gradients.gradients[index];
    10.                 previousGradient = index;
    11.             }
    12.         }
    Though no guarantee that it works from OnValidate.
     
  5. rayD8

    rayD8

    Joined:
    Mar 7, 2018
    Posts:
    32
    >> Well, the issue is not what is displayed.

    The issue IS what is displayed. That's the point of the video... to show the issue.
    The cause may not be what I thought. I may be handling it incorrectly... but...
    The issue is I want the gradient to be repainted on the script with the gradient selected, and it's not being repainted when changed in the editor UI. (clearly demonstrated in the video)

    The changes happen immediately in the play window (if playing) when the change is made to the field. Regardless of whether or not object with the script is deselected and re-selected or not. (not demonstrated in the video)

    >> The issue is that you're not correctly editing serialized objects inside the Unity editor. You're just forcefully changing the values behind the scenes without communicating your changes to the editor. Therefore those changes are not serialized.

    serializedObject.Update();
    serializedObject.ApplyModifiedProperties();
    Repaint();
    Were my attempts to notify the editor via the Editor script
    How would one go about "communicating your changes to the editor"?

    Also, why would the changes immediately happen in the play window if serialization was failing? (not contesting your statement... I just don't understand.)

    >> They could accidentally be serialized when another proper change happens which causes the object to be serialized.

    Perhaps a suggestion on how to properly notify? That would be helpful.

    >> You don't really should need the OnValidate method at all when you have a custom editor / PropertyDrawer. Your usage of gradientToUse and previousGradient is hacky at best. Though it could work when you do something like this:

    >> UnityEditor.Undo.RegisterObject(this);

    1) There is no "RegisterObject" method in the UnityEditor.Undo class.

    Any suggestion as to what should be added to the Editor script to notify of the change and properly serialize?
     
    Last edited: Apr 9, 2023
  6. rayD8

    rayD8

    Joined:
    Mar 7, 2018
    Posts:
    32
    Thank you, but that had the same behavior. No change to the gradient field in the editor until de-selecting and re-selecting the object with the script.
     
  7. rayD8

    rayD8

    Joined:
    Mar 7, 2018
    Posts:
    32
    I modified the code to include EditorGUI.BeginChangeCheck(), EditorGUI.EndChangeCheck(), EditorUtility.SetDirty(target), and Repaint().

    Changes are immediately detected in the Editor script, but a repaint of the gradient field still does not happen until clicking on a different gameObject and back, or clicking on another window and back. So the Editor class is detecting the change when it happens, as well as the changes being immediately detected and used in the Play window.

    It's just that the gradient field isn't being immediately repainted when changed.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. namespace Synesthesure
    4. {
    5.     public class GradientIssue_TEST : MonoBehaviour
    6.     {
    7.         [SerializeField] GradientLibrary gradients;
    8.         [SerializeField] int gradientToUse = -1;
    9.         [SerializeField] Gradient gradient;
    10.  
    11.         void Awake()
    12.         {
    13.             if (gradientToUse > -1 && gradients != null) SetGradient(gradientToUse);
    14.         }
    15.  
    16.         void OnValidate()
    17.         {
    18.             SetGradient(gradientToUse);
    19.         }
    20.  
    21.         public void SetGradient(int index)
    22.         {
    23.             if (gradients == null) return;
    24.             if (index > -1 && index < gradients.gradients.Length)
    25.             {
    26.                 gradient = gradients.gradients[index];
    27.             }
    28.         }
    29.     }
    30. }
    Code (CSharp):
    1. using UnityEditor;
    2.  
    3. namespace Synesthesure
    4. {
    5.     [CustomEditor(typeof(GradientIssue_TEST))]
    6.     public class GradientIssue_TEST_Editor : Editor
    7.     {
    8.         static bool showColorInfo = false;
    9.  
    10.         public override void OnInspectorGUI()
    11.         {
    12.             EditorGUI.BeginChangeCheck();
    13.  
    14.             serializedObject.Update();
    15.             showColorInfo = EditorGUILayout.Foldout(showColorInfo, "Color");
    16.  
    17.             if (showColorInfo)
    18.             {
    19.                 EditorGUILayout.PropertyField(serializedObject.FindProperty("gradients"));
    20.                 EditorGUILayout.PropertyField(serializedObject.FindProperty("gradientToUse"));
    21.                 EditorGUILayout.PropertyField(serializedObject.FindProperty("gradient"));
    22.             }
    23.             serializedObject.ApplyModifiedProperties();
    24.             if (EditorGUI.EndChangeCheck())
    25.             {
    26.                 EditorUtility.SetDirty(target);
    27.                 Repaint();  // <--- THIS DOES NOT REPAINT THE GRADIENT FIELD
    28.                 UnityEngine.Debug.Log("changed");
    29.             }
    30.         }
    31.     }
    32. }
    NOTE: I tried moving serializedObject.ApplyModifiedProperties(); to after the EditorGUI.EndChangeCheck() block and still got the same behavior.
     
  8. DouglasPotesta

    DouglasPotesta

    Joined:
    Nov 6, 2014
    Posts:
    108
    OnValidate is not meant for this.
    It’s not meant to change one property based on changes to a second property. Rather it’s meant to change the property that was changed.
    This should really have something like
    BeginChangeCheck
    …PropertyField…(GradientToUse)
    if EndChangeCheck
    Undo.Record target
    ((MyClass)target).ApplySelectedGradient();
    SetDirty(target)
     
  9. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    Well, you're right. Though Gradients are a bit tricky here. The issue is that generating the gradient texture that is shown in the inspector is quite expensive. That's why Unity uses an internal class called "GradientPreviewCache". This method has a method called ClearCache which will enforce a regeneration of the gradient textures when redrawn. As I said this class is internal. It's actually used by the Gradient editor which is meant at the only means how to "edit" Gradient instances in the editor. So in short your "manual" changes aren't anticipated by Unity when it comes to the the Gradient infrastructure.

    There is a hacky solution by simply calling ClearCache through reflection when you changed your gradient. You can use a class like this:

    Code (CSharp):
    1. public static class GradientCacheWrapper
    2. {
    3.     private static System.Action m_ClearCache = null;
    4.     public static void ClearCache()
    5.     {
    6. #if UNITY_EDITOR
    7.         if (m_ClearCache == null)
    8.         {
    9.             var gradientCacheType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditorInternal.GradientPreviewCache");
    10.             if (gradientCacheType == null)
    11.                 throw new System.Exception("Unity internal type GradientPreviewCache could not be found");
    12.             var clearCacheMethod = gradientCacheType.GetMethod("ClearCache", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
    13.             if (clearCacheMethod == null)
    14.                 throw new System.Exception("Unity internal type GradientPreviewCache doesn't have a method called ClearCache anymore");
    15.             m_ClearCache = (System.Action)System.Delegate.CreateDelegate(typeof(System.Action), clearCacheMethod);
    16.         }
    17.         m_ClearCache();
    18. #endif
    19.     }
    20. }
    You can simply use
    GradientCacheWrapper.ClearCache();
    whenever you want to update your gradient texture.
     
    rayD8 likes this.
  10. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    My bad, the method is called RecordObject. They are a bit inconsistent with the naming of their methods in that class ^^. Even though your changes kinda bypass Unity's serialized object system, since you actually change a serialized field properly (your gradientToUse field), the change to the gradient is captured as well at the end of the event since your object is already marked as dirty.

    Though keep in mind that you actually "copy" the selected gradient to your gradient variable. Well, it's a class and you actually store a reference, but when serialized and then deserialized, it would be a copy. So this kind of assignment can cause other issues down the line.

    It probably makes more sense to create your own wrapper type that encapsulates a GradientLibrary as well as a selected index and shows this as a single field. I'm not sure if you always want to use one of the values from the "gradient library" or if you want to be able to manually edit a detached gradient in place. If that is needed, this could be implemented as well.
     
    rayD8 likes this.
  11. rayD8

    rayD8

    Joined:
    Mar 7, 2018
    Posts:
    32
    My Gosh o_O No way I would have figured that out.
    That worked. Major QoL improvement in the tool. MANY THANKS! :)
     
    Last edited: Apr 10, 2023
  12. rayD8

    rayD8

    Joined:
    Mar 7, 2018
    Posts:
    32
    That's a good idea. I initially wanted to make a dropdown that would show all the gradients in the GradientLibrary, but couldn't figure out how to do it and needed to move forward.