Search Unity

  1. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  2. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

Observable variables in editor

Discussion in 'Immediate Mode GUI (IMGUI)' started by Fajlworks, Oct 10, 2017.

  1. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    Hi everyone,

    I was annoyed that Unity can't handle variable changes within editor inspector (e.g. update a Unity.UI.Text component when a string changes) by default. Because of that, updating a UnityEngine.UI.Text required me to reference it via script, which unnecessarily coupled data objects with views. Also, if I wanted to switch to TextMeshPRO later, I would have to refactor the code and link objects within scene again. Not to mention, if I wanted 2 or more UI components to share the value, that means even more references...

    So I made these little scripts that enable you to create an Observable type and make references to it whenever the value changes within the editor:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Events;
    3.  
    4. #if UNITY_EDITOR
    5. using UnityEditor;
    6.  
    7. namespace MyNamespace
    8. {
    9.     public abstract class ObservablePropertyDrawer<T> : PropertyDrawer
    10.     {
    11.         #region Variables
    12.  
    13.         bool showEvent = false;
    14.  
    15.         #endregion
    16.  
    17.  
    18.  
    19.         #region Abstract Methods
    20.  
    21.         protected abstract string ValuePropertyName { get; }
    22.         protected abstract string EventPropertyName { get; }
    23.         protected abstract void OnChangeOccured (Observable<T> observable, SerializedProperty valueProperty);
    24.  
    25.         #endregion
    26.  
    27.  
    28.  
    29.         #region Override Methods
    30.  
    31.         public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
    32.         {
    33.             var p = property.FindPropertyRelative (ValuePropertyName);
    34.             if (p == null)
    35.                 return base.GetPropertyHeight (property, label);
    36.  
    37.             float height = EditorGUI.GetPropertyHeight (p);
    38.  
    39.             if (showEvent)
    40.                 height += EditorGUI.GetPropertyHeight (property.FindPropertyRelative (EventPropertyName));
    41.  
    42.             return height;
    43.         }
    44.  
    45.         public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    46.         {
    47.             SerializedProperty valueProperty = property.FindPropertyRelative (ValuePropertyName);
    48.             if (valueProperty == null)
    49.             {
    50.                 EditorGUI.HelpBox (position, "No property named "+ValuePropertyName+" found!", MessageType.Error);
    51.                 return;
    52.             }
    53.  
    54.             EditorGUI.BeginChangeCheck();
    55.  
    56.             Rect valuePosition = position;
    57.             valuePosition.height = base.GetPropertyHeight (valueProperty, label);
    58.             valuePosition.xMax -= 20f;
    59.  
    60.             EditorGUI.PropertyField (valuePosition, valueProperty, new GUIContent(property.displayName));
    61.  
    62.             if (EditorGUI.EndChangeCheck())
    63.             {
    64.                 var observableObject = fieldInfo.GetValue(property.serializedObject.targetObject) as Observable<T>;
    65.                 OnChangeOccured (observableObject, valueProperty);
    66.             }
    67.  
    68.             Rect foldoutPosition = position;
    69.             foldoutPosition.height = valuePosition.height;
    70.             foldoutPosition.xMin = position.xMax;
    71.  
    72.             showEvent = EditorGUI.Foldout (foldoutPosition, showEvent, string.Empty);
    73.  
    74.             if (showEvent)
    75.             {
    76.                 var eventProperty = property.FindPropertyRelative (EventPropertyName);
    77.  
    78.                 Rect eventPosition = position;
    79.                 eventPosition.yMin += valuePosition.height;
    80.                 if (eventProperty != null)
    81.                 {
    82.                     EditorGUI.PropertyField (eventPosition, eventProperty);
    83.                 }
    84.                 else
    85.                 {
    86.                     EditorGUI.HelpBox (eventPosition, "No property named "+EventPropertyName+" found!", MessageType.Error);
    87.                 }
    88.  
    89.             }
    90.         }
    91.  
    92.         #endregion
    93.     }
    94. }
    95.  
    96. #endif
    97.  
    98. namespace MyNamespace
    99. {
    100.     [System.Serializable]
    101.     public class Observable<T>
    102.     {
    103.         #region Variables
    104.  
    105.         public event System.Action<ChangedArgs<T>> Changed;
    106.  
    107.         ChangedArgs<T> _args = new ChangedArgs<T>();
    108.         T _value;
    109.  
    110.         #endregion
    111.  
    112.  
    113.  
    114.         #region Properties
    115.  
    116.         public virtual T Value
    117.         {
    118.             set
    119.             {
    120.                 if (value != null ? value.Equals (_value) : _value == null)
    121.                     return;
    122.  
    123.                 T previous = _value;
    124.                 _value = value;
    125.  
    126.                 _args.Old = previous;
    127.                 _args.New = value;
    128.  
    129.                 if (Changed != null) Changed (_args);
    130.  
    131.                 HandleEventPropagation (_args);
    132.             }
    133.             get
    134.             {
    135.                 return _value;
    136.             }
    137.         }
    138.  
    139.         #endregion
    140.  
    141.  
    142.  
    143.         #region Virtual Methods
    144.  
    145.         protected virtual void HandleEventPropagation (ChangedArgs<T> args)
    146.         {
    147.          
    148.         }
    149.  
    150.         #endregion
    151.     }
    152.  
    153.     public struct ChangedArgs<T>
    154.     {
    155.         public T Old { get; internal set; }
    156.         public T New { get; internal set; }
    157.     }
    158. }
    Observable<T> can be used throughout code for same effect, but to be able to display it inside inspector (due to Unity serialization), we have to inherit the class for each type we want to display in editor. So, I premade a few most common types:

    ObservableBool:
    Code (CSharp):
    1. #pragma warning disable 414
    2.  
    3. using UnityEngine;
    4. using System.Collections;
    5.  
    6. #if UNITY_EDITOR
    7. using UnityEditor;
    8.  
    9. namespace MyNamespace.Editor
    10. {
    11.     [CustomPropertyDrawer (typeof (ObservableBool))]
    12.     internal class ObservableBoolPropertyDrawer : ObservablePropertyDrawer<bool>
    13.     {
    14.         protected override string EventPropertyName { get { return "OnChange"; } }
    15.  
    16.         protected override string ValuePropertyName { get { return "value"; } }
    17.  
    18.         protected override void OnChangeOccured (Observable<bool> observable, SerializedProperty valueProperty)
    19.         {
    20.             observable.Value = valueProperty.boolValue;
    21.         }
    22.     }  
    23. }
    24.  
    25. #endif
    26.  
    27. namespace MyNamespace
    28. {
    29.     [System.Serializable]
    30.     public class ObservableBool : Observable<bool>
    31.     {  
    32.         #region Declarations
    33.  
    34.         [System.Serializable] public class Event : UnityEngine.Events.UnityEvent<bool> { }
    35.  
    36.         #endregion
    37.  
    38.  
    39.  
    40.  
    41.         #region Variables
    42.  
    43.         [SerializeField]
    44.         bool value;
    45.  
    46.         public Event OnChange = new Event();
    47.  
    48.         #endregion
    49.  
    50.  
    51.  
    52.         #region Override Methods
    53.  
    54.         protected override void HandleEventPropagation (ChangedArgs<bool> args)
    55.         {
    56.             value = args.New;
    57.             OnChange.Invoke (args.New);
    58.         }
    59.  
    60.         #endregion
    61.     }
    62. }
    63.  

    ObservableInt:
    Code (CSharp):
    1. #pragma warning disable 414
    2.  
    3. using UnityEngine;
    4. using System.Collections;
    5.  
    6. #if UNITY_EDITOR
    7. using UnityEditor;
    8.  
    9. namespace MyNamespace.Editor
    10. {
    11.     [CustomPropertyDrawer (typeof (ObservableInt))]
    12.     internal class ObservableIntPropertyDrawer : ObservablePropertyDrawer<int>
    13.     {
    14.         protected override string EventPropertyName { get { return "OnChange"; } }
    15.  
    16.         protected override string ValuePropertyName { get { return "value"; } }
    17.  
    18.         protected override void OnChangeOccured (Observable<int> observable, SerializedProperty valueProperty)
    19.         {
    20.             observable.Value = valueProperty.intValue;
    21.         }
    22.     }  
    23. }
    24.  
    25. #endif
    26.  
    27. namespace MyNamespace
    28. {
    29.     [System.Serializable]
    30.     public class ObservableInt : Observable<int>
    31.     {  
    32.         #region Declarations
    33.  
    34.         [System.Serializable] public class Event : UnityEngine.Events.UnityEvent<int> { }
    35.  
    36.         #endregion
    37.  
    38.  
    39.  
    40.  
    41.         #region Variables
    42.  
    43.         [SerializeField]
    44.         int value;
    45.  
    46.         public Event OnChange = new Event();
    47.  
    48.         #endregion
    49.  
    50.  
    51.  
    52.         #region Override Methods
    53.  
    54.         protected override void HandleEventPropagation (ChangedArgs<int> args)
    55.         {
    56.             value = args.New;
    57.             OnChange.Invoke (args.New);
    58.         }
    59.  
    60.         #endregion
    61.     }
    62. }
    63.  

    ObservableFloat:
    Code (CSharp):
    1. #pragma warning disable 414
    2.  
    3. using UnityEngine;
    4. using System.Collections;
    5.  
    6. #if UNITY_EDITOR
    7. using UnityEditor;
    8.  
    9. namespace MyNamespace.Editor
    10. {
    11.     [CustomPropertyDrawer (typeof (ObservableFloat))]
    12.     internal class ObservableFloatPropertyDrawer : ObservablePropertyDrawer<float>
    13.     {
    14.         protected override string EventPropertyName { get { return "OnChange"; } }
    15.  
    16.         protected override string ValuePropertyName { get { return "value"; } }
    17.  
    18.         protected override void OnChangeOccured (Observable<float> observable, SerializedProperty valueProperty)
    19.         {
    20.             observable.Value = valueProperty.floatValue;
    21.         }
    22.     }  
    23. }
    24.  
    25. #endif
    26.  
    27. namespace MyNamespace
    28. {
    29.     [System.Serializable]
    30.     public class ObservableFloat : Observable<float>
    31.     {  
    32.         #region Declarations
    33.  
    34.         [System.Serializable] public class Event : UnityEngine.Events.UnityEvent<float> { }
    35.  
    36.         #endregion
    37.  
    38.  
    39.  
    40.         #region Variables
    41.  
    42.         [SerializeField]
    43.         float value;
    44.  
    45.         public Event OnChange = new Event();
    46.  
    47.         #endregion
    48.  
    49.  
    50.  
    51.         #region Override Methods
    52.  
    53.         protected override void HandleEventPropagation (ChangedArgs<float> args)
    54.         {
    55.             value = args.New;
    56.             OnChange.Invoke (args.New);
    57.         }
    58.  
    59.         #endregion
    60.     }
    61. }
    62.  

    ObservableString:
    Code (CSharp):
    1. #pragma warning disable 414
    2.  
    3. using UnityEngine;
    4. using System.Collections;
    5.  
    6. #if UNITY_EDITOR
    7. using UnityEditor;
    8.  
    9. namespace MyNamespace.Editor
    10. {
    11.     [CustomPropertyDrawer (typeof (ObservableString))]
    12.     internal class ObservableStringPropertyDrawer : ObservablePropertyDrawer<string>
    13.     {
    14.         protected override string EventPropertyName { get { return "OnChange"; } }
    15.  
    16.         protected override string ValuePropertyName { get { return "value"; } }
    17.  
    18.         protected override void OnChangeOccured (Observable<string> observable, SerializedProperty valueProperty)
    19.         {
    20.             observable.Value = valueProperty.stringValue;
    21.         }
    22.     }  
    23. }
    24.  
    25. #endif
    26.  
    27. namespace MyNamespace
    28. {
    29.     [System.Serializable]
    30.     public class ObservableString : Observable<string>
    31.     {  
    32.         #region Declarations
    33.  
    34.         [System.Serializable] public class Event : UnityEngine.Events.UnityEvent<string> { }
    35.  
    36.         #endregion
    37.  
    38.  
    39.  
    40.  
    41.         #region Variables
    42.  
    43.         [SerializeField]
    44.         string value;
    45.  
    46.         public Event OnChange = new Event();
    47.  
    48.         #endregion
    49.  
    50.  
    51.  
    52.         #region Override Methods
    53.  
    54.         protected override void HandleEventPropagation (ChangedArgs<string> args)
    55.         {
    56.             value = args.New;
    57.             OnChange.Invoke (args.New);
    58.         }
    59.  
    60.         #endregion
    61.     }
    62. }
    63.  
    and ObservableGameObject:
    Code (CSharp):
    1. #pragma warning disable 414
    2.  
    3. using UnityEngine;
    4. using System.Collections;
    5.  
    6. #if UNITY_EDITOR
    7. using UnityEditor;
    8.  
    9. namespace MyNamespace.Editor
    10. {
    11.     [CustomPropertyDrawer (typeof (ObservableGameObject))]
    12.     internal class ObservableGameObjectPropertyDrawer : ObservablePropertyDrawer<GameObject>
    13.     {
    14.         protected override string EventPropertyName { get { return "OnChange"; } }
    15.  
    16.         protected override string ValuePropertyName { get { return "value"; } }
    17.  
    18.         protected override void OnChangeOccured (Observable<GameObject> observable, SerializedProperty valueProperty)
    19.         {
    20.             observable.Value = valueProperty.objectReferenceValue as GameObject;
    21.         }
    22.     }  
    23. }
    24.  
    25. #endif
    26.  
    27. namespace MyNamespace
    28. {
    29.     [System.Serializable]
    30.     public class ObservableGameObject : Observable<GameObject>
    31.     {  
    32.         #region Declarations
    33.  
    34.         [System.Serializable] public class Event : UnityEngine.Events.UnityEvent<GameObject> { }
    35.  
    36.         #endregion
    37.  
    38.  
    39.  
    40.  
    41.         #region Variables
    42.  
    43.         [SerializeField]
    44.         GameObject value;
    45.  
    46.         public Event OnChange = new Event();
    47.  
    48.         #endregion
    49.  
    50.  
    51.  
    52.         #region Override Methods
    53.  
    54.         protected override void HandleEventPropagation (ChangedArgs<GameObject> args)
    55.         {
    56.             value = args.New;
    57.             OnChange.Invoke (args.New);
    58.         }
    59.  
    60.         #endregion
    61.     }
    62. }
    63.  
    there are double, Vector3, Vector2 and other MonoBehaviour types that could be added, so per need you can manually subclass and create your own.

    The end result looks like this:


    and when expanded on arrow, like this:


    Now you don't have to reference the component directly to update it! Sorry for the wall of text, but if anyone finds this useful like I did, great!

    I'm kinda disappointed it couldn't be something more concise and elegant like an [Attribute] above the variable. And another problem is that you can't directly observe, let's say, Transform.position.x change (you kinda have to update ObservableFloat within script to register change). Perhaps this will inspire others to improve the code.