Undo after adding a new child node is bugged

I am having this bug where if I add a child node and then press Ctrl Z to undo, it removes the child node as there is a missing reference but it still displays it in the editor.

I was wondering what the cause of this could be?

Most likely, the node lookup isn’t being rebuilt after the undo…

Paste in your DialogueEditor.cs and Dialogue.cs and we’ll take a look. Be sure to paste in the text of the scripts, and not a screenshot. Also, click on the MissingReferenceException message and paste in the stack trace that appears at the bottom of the Consol window. That can be a screenshot.

Dialogue.cs

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace RPG.Dialogue
{
    [CreateAssetMenu(fileName = "New Dialogue", menuName = "Dialogue", order = 0)]
    public class Dialogue : ScriptableObject
    {
        [SerializeField] List<DialogueNode> nodes = new List<DialogueNode>();
        Dictionary<string, DialogueNode> nodeLookup = new Dictionary<string, DialogueNode>();

#if UNITY_EDITOR
        private void Awake() 
        {
            if (nodes.Count == 0)
            {
                CreateNode(null);
            }

            OnValidate();
        }
#endif

        // Called when values of SO are changed or when it is loaded
        private void OnValidate() 
        {
            nodeLookup.Clear();
            foreach (DialogueNode node in GetAllNodes())
            {
                nodeLookup[node.name] = node;
            }
        }

        // Return IEnumerable because it allows the changing of the nodes type from List to another iterable type
        public IEnumerable<DialogueNode> GetAllNodes()
        {
            return nodes;
        }

        public DialogueNode GetRootNode()
        {
            return nodes[0];
        }

        public IEnumerable<DialogueNode> GetAllChildren(DialogueNode parentNode)
        {
            foreach (string childID in parentNode.children)
            {
                if (nodeLookup.ContainsKey(childID)) // If Dictionary contains this key return it
                {
                    yield return nodeLookup[childID]; // Repeats method and returns the next child to the node 
                }  
            }
        }

        public void CreateNode(DialogueNode parentNode)
        {
            DialogueNode newNode = CreateInstance<DialogueNode>();
            newNode.name = Guid.NewGuid().ToString();
            Undo.RegisterCreatedObjectUndo(newNode, "Created Dialogue Node");
            if (parentNode != null)
            {
                parentNode.children.Add(newNode.name);
            }
            nodes.Add(newNode);
            OnValidate();
        }
        
        public void DeleteNode(DialogueNode nodeToDelete)
        {
            nodes.Remove(nodeToDelete);
            OnValidate();
            CleanOrphanNodes(nodeToDelete);
            Undo.DestroyObjectImmediate(nodeToDelete);
        }

        private void CleanOrphanNodes(DialogueNode nodeToDelete)
        {
            foreach (DialogueNode node in GetAllNodes())
            {
                node.children.Remove(nodeToDelete.name);
            }
        }
    }
}

DialogueEditor.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using System;

namespace RPG.Dialogue.Editor
{
    public class DialogueEditor : EditorWindow {

        Dialogue selectedDialogue = null;
        Vector2 scrollPosition;
        [NonSerialized] GUIStyle nodeStyle = null;
        [NonSerialized] DialogueNode nodeToDrag = null;
        [NonSerialized] Vector2 draggingOffset;
        [NonSerialized] DialogueNode nodeToCreate = null;
        [NonSerialized] DialogueNode nodeToDelete = null;
        [NonSerialized] DialogueNode nodeToLink = null;
        [NonSerialized] bool isDraggingCanvas = false;
        [NonSerialized] Vector2 draggingCanvasOffset; 

        const float canvasSize = 4000;
        const float backgroundSize = 50;

        [MenuItem("Window/DialogueEditor")]
        private static void ShowEditorWindow() 
        {
            var window = GetWindow(typeof(DialogueEditor), false, "Dialogue Editor");
        }

        [OnOpenAssetAttribute(1)]
        public static bool OnOpenAsset(int instanceID, int line)
        {
            Dialogue dialogue = EditorUtility.InstanceIDToObject(instanceID) as Dialogue;
            if (dialogue != null)
            {
                ShowEditorWindow();
                return true;
            }
            return false;
        }

        private void OnEnable()
        {
            Selection.selectionChanged += OnSelectionChanged;
            nodeStyle = new GUIStyle();
            nodeStyle.normal.background = EditorGUIUtility.Load("node0") as Texture2D;
            nodeStyle.padding = new RectOffset(20, 20, 20, 20);
            nodeStyle.border = new RectOffset(12, 12, 12, 12);
        }

        private void OnSelectionChanged()
        {
            Dialogue newDialogue = Selection.activeObject as Dialogue;
            if (newDialogue != null)
            {
                selectedDialogue = newDialogue;
                Repaint();
            }
        }

        // Callback method that updates UI
        private void OnGUI() 
        {
            if (selectedDialogue == null)
            {
                Debug.Log("No dialogue selected");
            }
            else
            {
                ProcessEvents();

                scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
               
                Rect canvas = GUILayoutUtility.GetRect(canvasSize, canvasSize);
                Texture2D backgroundTexture = Resources.Load("background") as Texture2D;
                Rect textCoords = new Rect(0, 0, canvasSize/backgroundSize, canvasSize/backgroundSize);
                GUI.DrawTextureWithTexCoords(canvas, backgroundTexture, textCoords);

                foreach (DialogueNode node in selectedDialogue.GetAllNodes())
                {
                    DrawConnections(node);
                }
                foreach (DialogueNode node in selectedDialogue.GetAllNodes())
                {
                    DrawNode(node);
                }

                EditorGUILayout.EndScrollView();

                if (nodeToCreate != null)
                {
                    Undo.RecordObject(selectedDialogue, "Added Dialogue Node");
                    selectedDialogue.CreateNode(nodeToCreate);
                    nodeToCreate = null;
                }
                if (nodeToDelete != null)
                {
                    Undo.RecordObject(selectedDialogue, "Deleted Dialogue Node");
                    selectedDialogue.DeleteNode(nodeToDelete);
                    nodeToDelete = null;
                }
            }
        }

        private void ProcessEvents()
        {
            if (Event.current.type == EventType.MouseDown && nodeToDrag == null)
            {
                nodeToDrag = GetNodeAtPoint(Event.current.mousePosition + scrollPosition);
                if (nodeToDrag != null)
                {
                    draggingOffset = nodeToDrag.rect.position - Event.current.mousePosition;
                    Selection.activeObject = nodeToDrag;
                }
                else
                {
                    isDraggingCanvas = true;
                    draggingCanvasOffset = Event.current.mousePosition + scrollPosition; // The position of mouse on canvas
                    Selection.activeObject = selectedDialogue;
                }
            }
            else if (Event.current.type == EventType.MouseDrag && nodeToDrag != null)
            {
                Undo.RecordObject(selectedDialogue, "Move Dialogue Node");
                nodeToDrag.rect.position = Event.current.mousePosition + draggingOffset;
               
                GUI.changed = true; // Triggers OnGUI to be called again
            }
            else if (Event.current.type == EventType.MouseDrag && isDraggingCanvas)
            {
                scrollPosition = draggingCanvasOffset - Event.current.mousePosition;

                GUI.changed = true; // Triggers OnGUI to be called again
            }
            else if (Event.current.type == EventType.MouseUp && nodeToDrag != null)
            {
                nodeToDrag = null;     
            }
            else if (Event.current.type == EventType.MouseUp && isDraggingCanvas)
            {
                isDraggingCanvas = false;
            }

        }

        private void DrawNode(DialogueNode node)
        {
            GUILayout.BeginArea(node.rect, nodeStyle);
            EditorGUI.BeginChangeCheck();

            string newText = EditorGUILayout.TextField(node.text);

            if (EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(selectedDialogue, "Update Dialogue Text");
                node.text = newText;
            }

            GUILayout.BeginHorizontal();
            if (GUILayout.Button("+"))
            {
                nodeToCreate = node;
            }
            DrawLinkButtons(node);
            if (GUILayout.Button("-"))
            {
                nodeToDelete = node;
            }
            GUILayout.EndHorizontal();

            GUILayout.EndArea();
        }

        private void DrawLinkButtons(DialogueNode node)
        {
            if (nodeToLink == null)
            {
                if (GUILayout.Button("edit links"))
                {
                    nodeToLink = node;
                }
            }
            else if (nodeToLink == node)
            {
                if (GUILayout.Button("cancel"))
                {
                    nodeToLink = null;
                }
            }
            else if (nodeToLink.children.Contains(node.name))
            {
                 if (GUILayout.Button("unlink"))
                {
                    Undo.RecordObject(selectedDialogue, "Add Dialogue Link");
                    nodeToLink.children.Remove(node.name);
                    nodeToLink = null;
                }
            }
            else
            {
                if (GUILayout.Button("link"))
                {
                    Undo.RecordObject(selectedDialogue, "Add Dialogue Link");
                    nodeToLink.children.Add(node.name);
                    nodeToLink = null;
                }
            }
        }

        private void DrawConnections(DialogueNode node)
        {
            Vector3 startPosition = new Vector2(node.rect.xMax, node.rect.center.y);
            foreach (DialogueNode childNode in selectedDialogue.GetAllChildren(node))
            {
                Vector3 endPosition = new Vector2(childNode.rect.xMin, childNode.rect.center.y);
                Vector3 controlPointOffset = endPosition - startPosition;
                controlPointOffset.y = 0;
                controlPointOffset.x *= 0.8f;
                Handles.DrawBezier(
                    startPosition, endPosition,
                    startPosition + controlPointOffset, 
                    endPosition - controlPointOffset, 
                    Color.white, null, 4f);
            }
        }


        private DialogueNode GetNodeAtPoint(Vector2 point)
        {
            DialogueNode foundNode = null; // Do not return the first found node as there could be another node overlapping it
            foreach (DialogueNode node in selectedDialogue.GetAllNodes())
            {
                if (node.rect.Contains(point))
                {
                    foundNode = node;
                }
            }
            // Return foundNode only after finishing iterating all the nodes (prevent missing out on the overlapping nodes with higher index)
            return foundNode;
        }
    }
}

stack trace for NullReferenceException

NullReferenceException: Object reference not set to an instance of an object.
RPG.Dialogue.Editor.DialogueEditor.DrawConnections (RPG.Dialogue.DialogueNode node) (at Assets/Scripts/Dialogue/Editor/DialogueEditor.cs:214)
RPG.Dialogue.Editor.DialogueEditor.OnGUI () (at Assets/Scripts/Dialogue/Editor/DialogueEditor.cs:83)
UnityEditor.HostView.InvokeOnGUI (UnityEngine.Rect onGUIPosition) (at <5f40cdb07bd44d76a23dad985a4ec283>:0)
UnityEditor.DockArea.DrawView (UnityEngine.Rect dockAreaRect) (at <5f40cdb07bd44d76a23dad985a4ec283>:0)
UnityEditor.DockArea.OldOnGUI () (at <5f40cdb07bd44d76a23dad985a4ec283>:0)
UnityEngine.UIElements.IMGUIContainer.DoOnGUI (UnityEngine.Event evt, UnityEngine.Matrix4x4 parentTransform, UnityEngine.Rect clippingRect, System.Boolean isComputingLayout, UnityEngine.Rect layoutSize, System.Action onGUIHandler, System.Boolean canAffectFocus) (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.IMGUIContainer.HandleIMGUIEvent (UnityEngine.Event e, UnityEngine.Matrix4x4 worldTransform, UnityEngine.Rect clippingRect, System.Action onGUIHandler, System.Boolean canAffectFocus) (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.IMGUIContainer.DoIMGUIRepaint () (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.UIR.RenderChainCommand.ExecuteNonDrawMesh (UnityEngine.UIElements.UIR.DrawParams drawParams, System.Single pixelsPerPoint, System.Exception& immediateException) (at <e262c2d839014c8090373617ef295bab>:0)
Rethrow as ImmediateModeException
UnityEngine.UIElements.UIR.RenderChain.Render () (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.UIRRepaintUpdater.Update () (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.VisualTreeUpdater.UpdateVisualTreePhase (UnityEngine.UIElements.VisualTreeUpdatePhase phase) (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.Panel.UpdateForRepaint () (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.Panel.Repaint (UnityEngine.Event e) (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.UIElementsUtility.DoDispatch (UnityEngine.UIElements.BaseVisualElementPanel panel) (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.UIElementsUtility.UnityEngine.UIElements.IUIElementsUtility.ProcessEvent (System.Int32 instanceID, System.IntPtr nativeEventPtr, System.Boolean& eventHandled) (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.UIEventRegistration.ProcessEvent (System.Int32 instanceID, System.IntPtr nativeEventPtr) (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.UIElements.UIEventRegistration+<>c.<.cctor>b__1_2 (System.Int32 i, System.IntPtr ptr) (at <e262c2d839014c8090373617ef295bab>:0)
UnityEngine.GUIUtility.ProcessEvent (System.Int32 instanceID, System.IntPtr nativeEventPtr, System.Boolean& result) (at <67dcbe01fcb94a00b87487037550ce1c>:0)

Thanks in advance for the help! I first thought the children were not being properly removed even though the node is removed from the nodes list in general but I put debug messages just before the Remove() and it seemed to be removing the proper node as it was printing the correct name before removing.

private void CleanOrphanNodes(DialogueNode nodeToDelete)
        {
            foreach (DialogueNode node in GetAllNodes())
            {
                node.children.Remove(nodeToDelete.name);
            }
        }

It’s quite possible that OnValidate() is not being called properly when Undo is pressed. This could be something changed in later versions of Unity.

For now, let’s put in some null checking that we probably should have added when we wrote the course.

In DrawConnections()

        private void DrawConnections(DialogueNode node)
        {
            Vector3 startPosition = new Vector2(node.rect.xMax, node.rect.center.y);
            foreach (DialogueNode childNode in selectedDialogue.GetAllChildren(node))
            {
                if(childNode==null) continue;
                Vector3 endPosition = new Vector2(childNode.rect.xMin, childNode.rect.center.y);
                Vector3 controlPointOffset = endPosition - startPosition;
                controlPointOffset.y = 0;
                controlPointOffset.x *= 0.8f;
                Handles.DrawBezier(
                    startPosition, endPosition,
                    startPosition + controlPointOffset, 
                    endPosition - controlPointOffset, 
                    Color.white, null, 4f);
            }
        }

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.

Privacy & Terms