I think this bug affects more than just Sub Assets

In this video, @sampattuzzi mentions that this bug only seems to affect “sub assets”, however I think that this might not be completely accurate.

For the last few hours I have battled with the ScriptableObject saving system, and from what I can tell, even at the top level (selectedDialogue in DialogueEditor.cs) the only way to get your changes onto disk is to do at least one Undo or Redo action before you attempt to save (save project or save scene, either seems to work). If you create a brand new Dialogue asset and never Undo or Redo when editing it, nothing I do seems to get it written to disk. But then one Undo, save, and bam… saved to disk. To save another change, you need to do another preceding Undo operation. Even moving a Node and undo-ing it is sufficient.

Adding this EditorUtilities.SetDirty(object) just after all Undo.RecordObject() calls seems to be the only way I can ensure that changes end up on disk without at least one undo. So adding this throughout DialogueEditor.cs seems to be the way to go. A wrapper function that calls Undo.RecordObject() and EditorUtilities.SetDirty(object) is probably the best option at this point, assuming that Unity doesn’t pre-emptively interrupt Editor code (it might? Who knows…)

I am using watch -n1 -d=permanent "cat 'New Dialogue.asset'" to see exactly when changes go to disk.

I’ve reproduced this with the course github repository.

I’m wondering if this might be related to no “starting position” being saved by the Undo system - do we need to somehow create a baseline Undo record in Dialogue.cs's Awake() function so that Undo.RecordObject() has something to diff against? Perhaps until you attempt to actually undo or redo the dirty flag isn’t properly set by Undo.RecordObject()? Pure speculation at this point… I played with Undo.RegisterCompleteObjectUndo() in Dialogue.Awake() to try and create a baseline but I wasn’t successful, yet.

EDIT: observation - if I edit the SO data via the Inspector it will save to disk immediately on a Save command, no Undo required, which is what I’d expect to see there.

1 Like

The behaviour of the Undo system is really poorly documented. Thanks for doing the sleuth work there. I think that marking Dirty probably is the failsafe solution. I wonder why they recommend against it in the docs.

1 Like

I agree with you Sam. From what I found in the docs, it’s a claim that Undo.RecordObject is sufficient to mark an object as dirty, which might be true for most objects, but doesn’t seem to be true for ScriptableObjects. So it may just be an accident of omission on Unity’s part.

For anyone else coming across this, so far I’ve found this simple static function to be sufficient, used instead wherever the course code was previously calling Undo.RecordObject():

    public class UndoAndDirty
    {
        public static void Mark(UnityEngine.Object target, string undoDescription)
        {
            Undo.RecordObject(target, undoDescription);
            EditorUtility.SetDirty(target);
        }
    }

Usefully it seems that setting the dirty flag just before changing the SO state has the same end effect as changing it just after, allowing both calls to be combined like this.

I also added this to the UI, which lets me see when the selected SO object in the Editor window is dirty:

EditorGUILayout.LabelField(selectedDialogue.name + (EditorUtility.IsDirty(selectedDialogue) ? " *" : ""));

Therefore after making a change, the * appears, and then disappears after Saving.

2 Likes

Really awesome additions. Thanks for sharing them!

In my own work with the Undo/Dirty system, I have found that without the SetDirty(), there is simply no consistency to whether or not the change will actually be saved. I think Unity made a mistake and meant to recommend using it, not recommend against it.

I like the looks of this static function… where would I put it?
Should I make a new script UndoAndDirty.cs?

public class UndoAndDirty
    {
        public static void Mark(UnityEngine.Object target, string undoDescription)
        {
            Undo.RecordObject(target, undoDescription);
            EditorUtility.SetDirty(target);
        }
    }

Also the UI mentioned here
Where to add this line?

EditorGUILayout.LabelField(selectedDialogue.name + (EditorUtility.IsDirty(selectedDialogue) ? " *" : ""));

Looks like a useful thing to implement into our project…
Is it necessary?
Will it help?
Thanks

Let’s take the UndoAndDirty a bit further… make it more broad based (and make it so you don’t actually have to fully qualify that class… Put this script in an Editor folder

public static class UndoAndDirty
{
     public static void Mark(this UnityEngine.Object target, string undoDescription)
     {
          Undo.RecordObject(target, undoDescription);
          EditorUtility.SetDirty(target);
      }
}

For the second question… the issue is getting that line to show up in your dialogue, which it won’t because we paint the dialogue in a different way… you could change the window, however:

            string content = "Dialogue Editor";
            if (selectedDialogue)
            {
                content = selectedDialogue.name + (EditorUtility.IsDirty(selectedDialogue) ? " *" : "");
            }
            titleContent = new GUIContent(content);

That little fragment would go at the end of OnGUI()

Of course, if you’re prompt about taking care of Undos and SetDirtys, you will find that the window title never gets a *.

I haven’t tried this, but looking at the Unity documentation, in the same place they recommend against SetDirty, they say:

When you create editor UI for manipulating an object, such as a custom editor to modify serialized properties on a component or asset, if possible, you should use the SerializedProperty system using SerializedObject.FindProperty, SerializedObject.Update, EditorGUILayout.PropertyField, and SerializedObject.ApplyModifiedProperties. This will automatically mark the object as dirty, create an undo entry, and ensure Prefab overrides are created if relevant.

I’m guessing (or rather, I hope) that’s the way to go instead of using SetDirty, but at the moment I’m just following the RPG Dialogue course and I’m not that worried about it.

Ideally, you’re correct, most Editor code is written using SerializedProperties, which automatically handle both Undo and Dirty for you.

Unfortunately, much of the code we’re working with doesn’t work well with SerializedProperties. I’m not saying it can’t be done (it definitely can), but it will add significantly to the code.

At some point, I’ll see if I can work up an alternate Dialogue editor using SerializedProperties. My new version of the InventoryItemEditor soon to be released (using the new UI Toolkit) does rely entirely on SerializedProperties.

Like @Brian_Trotter has said this implementation we are not using Serialized Objects / Properties. The Objects are Serialized Objects and the Properties are Serialized properties, but we are not working with them as serialized Objects / Properties in the Editor Window, for that we would have to track the serialized object instead of the Dialogue Object.

Drawing of the Nodes
        private Dialogue _dialogue;
        private SerializedObject _serializedDialogue;

        private void OnSelectionChange()
        {
            if (Selection.activeObject is not Dialogue dialogue) return;
            _dialogue = dialogue;
            _serializedDialogue = new SerializedObject(dialogue); 
            
            Repaint();
        }

        private void DrawNode(DialogueNode node)
        {
            if (!node) return;
            var nodeName = !string.IsNullOrWhiteSpace(node.NickName)
                ? node.NickName
                : node.name[^12..];

            SerializedObject serializedNode =  new(node);
            SerializedProperty messageProperty = serializedNode.FindProperty("message");

            using (new GUILayout.AreaScope(node.Rect, "", _nodeStyle))
            {
                EditorGUILayout.LabelField(nodeName, EditorStyles.centeredGreyMiniLabel);

                //node.Message = EditorGUILayout.TextArea(node.Message, _textBoxStyle); 
                EditorGUILayout.PropertyField(messageProperty);
                // or if you prefer;
                //messageProperty.stringValue = EditorGUILayout.TextArea(node.Message, _textBoxStyle);

                using (new EditorGUILayout.HorizontalScope())
                {
                    DrawRemoveButton();
                    DrawAddButton();
                }

                DrawLinkButtons(node);
            }

            serializedNode.ApplyModifiedProperties();
        }

I would use property if it was me because in my Dialogue Node I Have

        [SerializeField, TextArea(minLines: 2, maxLines: 10)]
        private string message;

Use property field would automatically draw a text area, because it takes into account the attributes on the Serialized Fields.

In Process Events instead of _draggingNode.Position = current.mousePosition + _draggingOffset;
we would have to get the serialized property of the node that we are dragging

                    SerializedObject so = new(_draggingNode);
                    SerializedProperty positionProperty = so.FindProperty("screenRect");
                    Rect rect = positionProperty.rectValue;
                    rect.position = current.mousePosition + _draggingOffset;
                    positionProperty.rectValue = rect;
                    so.ApplyModifiedProperties();

The process would be similar any where that we wanted to modify values in our editor script and automatically handle Undo Redo set Dirty.

Using the UI Tool Kit for the Dialogue Editor

I love the new UI Tool Kit I was thinking about converting the Dialogue Editor over to Visual Elements after I finished the complete section on the UI in the course.

I have not decided if I will use the UI Buillder/UXML or just do it all pure code. I was thinking about using the Experimental Graph view so I might do some of it as code and some of it in UI Builder. I haven’t gotten it planed yet.

After Yesterdays Unite talk I can not wait to use the UI Tool Kit in 2022, they have added some cool features. They are going to make us wait until l 2023 for the coll added Binding system that makes binding in the Builder 100000% easier.

Privacy & Terms