Search Unity

Serialization, Undo, and ApplyModifiedPropertiesWithoutUndo

Discussion in 'Scripting' started by theHost, Jul 25, 2017.

  1. theHost

    theHost

    Joined:
    Nov 30, 2009
    Posts:
    37
    After looking through docs, this forum, and a lot of searching, I guess it is time to post here. So the gist of the problem is that SerializedObject.ApplyModifiedPropertiesWithoutUndo is not working as expected. It probably is something I am not doing right, or perhaps a bug with ApplyModifiedPropertiesWithoutUndo?

    I have a RootObject derived from MonoBehaviour. It has a public reference to ChildObject, which is derived from ScritableObject. I have a RootUI derived from Editor which is the custom editor window for RootObject. The idea is that I want to display both RootObject and ChildObject properties using just the RootUI editor window. I am using SerializedProperty to display the values, and have it automatically handle undo.

    The issue comes up with Undo. In ChildObject, I have 2 ints. One of them (external int) is used to display to the user. The other int (internal) is to track changes, and if its different from the external int, I do some work and update the internal value after work is done. I then use ApplyModifiedPropertiesWithoutUndo to save the ChildObject state, believing that the internal value's change will not be part of the undo record.

    When undo happens, I have to do some work again with the changed values, but I need to know which values were undone. The problem is that in my undo handler, both my internal and external ints are the same (and have already been updated to previous value). I would expect that only the external one should have changed, not both.

    Simplified version of the code below:

    RootObject:
    Code (CSharp):
    1. public class RootObject : MonoBehaviour
    2. {
    3.     public ChildObject _childObject;
    4.  
    5.     public void Initialize()
    6.     {
    7.         if(_childObject == null)
    8.         {
    9.             _childObject = ScriptableObject.CreateInstance<ChildObject>();
    10.         }
    11.     }
    12.  
    13. }
    ChildObject:
    Code (CSharp):
    1. public class ChildObject : ScriptableObject
    2. {
    3.     [SerializeField]
    4.     private int _internalInt;
    5.  
    6.     [SerializeField]
    7.     private int _externalInt;
    8.  
    9.  
    10.     public bool CheckInts()
    11.     {
    12.         Debug.LogFormat("CheckInt:: Internal: {0}, External: {1}", _internalInt, _externalInt);
    13.         return (_internalInt != _externalInt);
    14.     }
    15.  
    16.     public void UpdateInternal()
    17.     {
    18.         _internalInt = _externalInt;
    19.         Debug.LogFormat("UpdateInternal:: Internal: {0}, External: {1}", _internalInt, _externalInt);
    20.     }
    21. }
    22.  
    RootUI (Custom Editor):
    Code (CSharp):
    1. [CustomEditor(typeof(RootObject))]
    2. public class RootUI : Editor
    3. {
    4.     // This is a custom editor to display both RootObject and ChildObject Inspector UI
    5.  
    6.     public RootObject _rootObject;
    7.     public ChildObject _childObject;
    8.  
    9.  
    10.     public void OnEnable()
    11.     {
    12.         _rootObject = target as RootObject;
    13.  
    14.         // Make sure root object has a child
    15.         _rootObject.Initialize();
    16.         _childObject = _rootObject._childObject;
    17.     }
    18.  
    19.     public override void OnInspectorGUI()
    20.     {
    21.         serializedObject.Update();
    22.  
    23.         bool guiEnabled = GUI.enabled;
    24.  
    25.         EditorGUI.BeginChangeCheck();
    26.  
    27.         // Serialize the child object
    28.         SerializedObject childSerializedObject = new SerializedObject(_childObject);
    29.  
    30.         // Create serialized property to display the int slider
    31.         SerializedProperty childExternalInt = childSerializedObject.FindProperty("_externalInt");
    32.         EditorGUILayout.IntSlider(childExternalInt, 0, 10);
    33.  
    34.         // If there were changes, and our ints are different, then update the internal int
    35.         if (EditorGUI.EndChangeCheck())
    36.         {
    37.             // Apply any changes
    38.             childSerializedObject.ApplyModifiedProperties();
    39.             serializedObject.ApplyModifiedProperties();
    40.  
    41.             if (_childObject.CheckInts())
    42.             {
    43.                 childSerializedObject.Update();
    44.  
    45.                 // Get the external and internal int properties
    46.                 childExternalInt = childSerializedObject.FindProperty("_externalInt");
    47.                 SerializedProperty childInternalInt = childSerializedObject.FindProperty("_internalInt");
    48.  
    49.                 // Update the internal int using external int's value
    50.                 int oldInternal = childInternalInt.intValue;
    51.                 childInternalInt.intValue = childExternalInt.intValue;
    52.                 Debug.LogFormat("Internal old: {0}, new: {1}: ", oldInternal, childInternalInt.intValue);
    53.  
    54.                 // Disable undo on this change
    55.                 childSerializedObject.ApplyModifiedPropertiesWithoutUndo();
    56.             }
    57.         }
    58.  
    59.         GUI.enabled = guiEnabled;
    60.     }
    61.  
    62.  
    63.     public void OnSceneGUI()
    64.     {
    65.         if ((Event.current.type == EventType.ValidateCommand && Event.current.commandName.Equals("UndoRedoPerformed")))
    66.         {
    67.             Event.current.Use();
    68.         }
    69.  
    70.         if ((Event.current.type == EventType.ExecuteCommand && Event.current.commandName.Equals("UndoRedoPerformed")))
    71.         {
    72.             // Handle Undo where we want to check the difference between external and internal int
    73.  
    74.             SerializedObject childSerializedObject = new SerializedObject(_childObject);
    75.  
    76.             SerializedProperty childExternalInt = childSerializedObject.FindProperty("_externalInt");
    77.             SerializedProperty childInternalInt = childSerializedObject.FindProperty("_internalInt");
    78.  
    79.             // Check property values
    80.             Debug.LogFormat("Undo:: Internal: {0}, External: {1}", childInternalInt.intValue, childExternalInt.intValue);
    81.  
    82.             // Check actual object int values
    83.             _childObject.CheckInts();
    84.         }
    85.  
    86.     }
    87.  
    88. }
    RootUI::OnSceneGUI is where I handle the undo event. The ints are the same. Any ideas?

    Using Unity 5.6
     
    Last edited: Jul 25, 2017
  2. theHost

    theHost

    Joined:
    Nov 30, 2009
    Posts:
    37
    Wondering if anyone has any ideas.