Dialogue window keeps disappearing in the dialogue editor

Hi, I have a problem, can I ask you please? When I created new dialogue by scriptable objects and click on the dialog (or deselect dialogue in the project window) in dialogue editor the dialogue in the editor disappears. Where is the problem? Unity version? I have this one: 2022.3.5. I think, I have the code same like in the tutorial. I’ve tried to follow this topic: bunch-of-errors-after-deleting-the-4-dialogues, but there were some errors

Here are the scripts:

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

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>();

        // make sure awake is called in the editor, not in the game built etc. 
#if UNITY_EDITOR
        private void Awake()
        {
            if (nodes.Count == 0)
            {
                CreateNode(null);
            }
        }
#endif

        private void OnValidate()
        {
 
            nodeLookup.Clear();
            foreach (DialogueNode node in GetAllNodes())
            {
                nodeLookup[node.name] = node;
            }
        }

        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))
                {
                    yield return nodeLookup[childID];
                }
            }
        }

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

        public void DeleteNode(DialogueNode nodeToDelete)
        {
            nodes.Remove(nodeToDelete);
            Undo.DestroyObjectImmediate(nodeToDelete);
            OnValidate();
            CleanDanglingChildren(nodeToDelete);
        }

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


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

namespace RPG.Dialogue
{
    public class DialogueNode : ScriptableObject
    {
        public string text;
        public List<string> children = new List<string>();       
        public Rect rect = new Rect(0, 0, 200, 100);
    }
}

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

namespace RPG.Dialogue.Editor
{
    public class DialogueEditor : EditorWindow
    {
        Dialogue selectedDialogue = null;
        [NonSerialized] GUIStyle nodeStyle;
        [NonSerialized] DialogueNode draggingNode = null;
        [NonSerialized] Vector2 draggingOffset;
        [NonSerialized] DialogueNode creatingNode = null;
        [NonSerialized] DialogueNode deletingNode = null;
        [NonSerialized] DialogueNode linkingParentNode = null;
        Vector2 scrollPosition;
        [NonSerialized] bool draggingCanvas = false;
        [NonSerialized] Vector2 draggingCanvasOffset;

        const float canvasSize = 400;
        const float backgroundSize = 50;

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

        // Callback that gets called when an asset is double-clicked in the Unity editor. In this case, if the asset is a Dialogue object, it opens the dialogue editor window.
        [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(); // if user selects dialogue it is updated also in the Dialogue window
            }
        }

        void OnGUI()
        {
            if (selectedDialogue == null)
            {
                EditorGUILayout.LabelField("No dialogue selected");
            }
            else
            {
                ProcessEvents();

                scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

                Rect canvas = GUILayoutUtility.GetRect(canvasSize, canvasSize);
                Texture2D backgroundTex = Resources.Load("background") as Texture2D;
                Rect texCoords = new Rect(0, 0, canvasSize / backgroundSize, canvasSize / backgroundSize);
                GUI.DrawTextureWithTexCoords(canvas, backgroundTex, texCoords);

                // if two following foreachs are only one, bezier curve is on top of the node rectangle
                foreach (DialogueNode node in selectedDialogue.GetAllNodes())
                {
                    DrawConnections(node);
                }
                foreach (DialogueNode node in selectedDialogue.GetAllNodes())
                {
                    DrawNode(node);
                }
                EditorGUILayout.EndScrollView();

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

        }

        private void ProcessEvents()
        {
            if (Event.current.type == EventType.MouseDown && draggingNode == null)
            {
                draggingNode = GetNodeAtPoint(Event.current.mousePosition + scrollPosition);
                if (draggingNode != null)
                {
                    draggingOffset = draggingNode.rect.position - Event.current.mousePosition;
                    //------------ 13/12
                    Selection.activeObject = draggingNode;
                }
                // Record drag offset and dragging
                else
                {
                    draggingCanvas = true;
                    draggingCanvasOffset = Event.current.mousePosition + draggingOffset;
                    Selection.activeObject = selectedDialogue;
                }
            }
            else if (Event.current.type == EventType.MouseDrag && draggingNode != null)
            {
                Undo.RecordObject(selectedDialogue, "Move Dialogue Node");
                draggingNode.rect.position = Event.current.mousePosition + draggingOffset;
      
                GUI.changed = true;  // or: Repaint();    
            }
            //Update scrollPosition
            else if (Event.current.type == EventType.MouseDrag && draggingCanvas)
            {
                scrollPosition = draggingCanvasOffset - Event.current.mousePosition;
                GUI.changed = true;
            }

            else if (Event.current.type == EventType.MouseUp && draggingNode != null)
            {
                draggingNode = null;
            }
            else if (Event.current.type == EventType.MouseUp && draggingCanvas)
            {
                draggingCanvas = 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(new GUIContent("+", "Button"), new GUIStyle(GUI.skin.button) { normal = new GUIStyleState() { textColor = Color.green } }))
            {
                creatingNode = node;
            }

            DrawLinkButtons(node);

            if (GUILayout.Button("-"))
            {
                deletingNode = node;
            }
            GUILayout.EndHorizontal();

            GUILayout.EndArea();
        }

        private void DrawLinkButtons(DialogueNode node)
        {
            if (linkingParentNode == null)
            {
                if (GUILayout.Button("link"))
                {
                    linkingParentNode = node;
                }
            }
            else if (linkingParentNode == node)
            {
                 if (GUILayout.Button("cancel"))
                {
                    linkingParentNode = null;
                }
            }
            else if (linkingParentNode.children.Contains(node.name))
            {
                 if (GUILayout.Button("Unlink"))
                {
                    Undo.RecordObject(selectedDialogue, "Remove Dialogue Link");
                    linkingParentNode.children.Remove(node.name);
                    linkingParentNode = null;
                } 
            }
            else
            {
                if (GUILayout.Button("child"))
                {
                    Undo.RecordObject(selectedDialogue, "Add Dialogue Link");
                    linkingParentNode.children.Add(node.name);
                    linkingParentNode = 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);
                // Nice drawing of bezier curves:
                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;
            foreach (DialogueNode node in selectedDialogue.GetAllNodes())
            {
                if (node.rect.Contains(point))
                {
                    foundNode = node;
                }
            }
            return foundNode;
        }


    }
}


This is likely a recurring bug with the Jobs system that Unity is now using behind the scenes with the Editor, which is interfering with automatic node creation.

Try adding this to DialogueEditor.OnGUI() before ProcessEvents:

if(selectedDialogue.GetAllNodes().FirstOrDefault==null)
{
    selectedDialogue.CreateNode(null);
}

(You’ll also need to add using System.Linq; to your usings clauses)

using System.Linq;

Hi Brian,

thank you very much, it is working. Only think I needed to change in your script was: FirstOrDefault to FirstOrDefault().

So final code is this:

if (selectedDialogue.GetAllNodes().FirstOrDefault() == null)
{
   selectedDialogue.CreateNode(null);
}

Sorry, I was typing that response from my work computer. Though I use FirstOrDefault() enough to know it is a method, and not a property. Glad you’re back up and running.

What the h…?!
I lost about 2 hours looking for a solution… or, actually, looking for what I was doing wrong…

@Brian_Trotter: Once again, thank you for bringing in the solution!

Still, it feels like there is some kind of black magic going in there… Brian shows up and solves the problem!

Could you please elaborate your answer?
What is going on under the hood?
What put you on the trail to the solution?
I want to understand the “why” more than the “what”…
PS: don’t get me wrong: I REALLY appreciate all the help and solutions you bring to us… but I just try to understand. I won’t have a Solving-All-My-C#-Problems-Brian in my pocket for ever;)
Thanks a lot!

PS: if I understand well, if we go with the solution that you propose, the creation of a root node in the Awake method of the Dialogue class (see below) is now obsolete, right?

private void Awake()
{
    if (nodes.Count == 0)
    {
        CreateEmptyNode(null);
    }
...

In the case of this solution, it took me FAR longer than a couple hours to craft a solution, but I crafted that solution quite a while ago when Unity first started using Worker tasks (Jobs) in their Editor code.
It took me a while to figure out that while one thread had a copy of the ScriptableObject without validation that was waiting for us to cleverly give the SO a name, another thread was holding on to a copy that had that created node.

So the node is created, satisfying our Awake() method, but when it’s renamed from [default name] to it’s real name, that SO’s DATA is discarded in favor of the uninitialized data in the copy you just named.

From there, it was a matter of setting a trap in the DialogueEditor to find out if there were no nodes, and if there were no nodes, then we need to create one.

You’ll have one as long as you’re working through a course I TA. :slight_smile:

I really don’t want to know how you figured this out (actually, I would definitely LOVE to know but I feel like it will be far beyond the field of this course… so I won’t ask :no_mouth:).
Anyway, I am not sure to understand the implications of your statement.
Do you mean that any SO data modification done in the Awake method is only made in the temporary copy of the SO that will be destroyed as soon as you validate the name of the asset?
In other words: any modification of the SO data made in the Awake method is worthless?

=> :I_worship_you:

In fact, I hate going to bed without having found the answers to my questions.
Furthermore, I ran into a strange behavior while following the next lecture (about sub assets), so I decided to investigate more.

I added a lot of Debug.Log into the Dialog.cs’s methods:

        private void Awake()
        {
            Debug.Log("We are in Awake");
            OnValidate();
        }

and

public DialogueNode CreateEmptyNode(DialogueNode parentNode)
{
    Debug.Log("We are in CreateEmptyNode...");

    DialogueNode createdNode = ScriptableObject.CreateInstance<DialogueNode>();

    createdNode.name = System.Guid.NewGuid().ToString();
    Debug.Log($"Creating node {createdNode.name}");

    Undo.RegisterCreatedObjectUndo(createdNode, "Created dialogue node");

    if (parentNode != null)
        parentNode.AddChild(createdNode);
    //Ajout dans la liste des noeuds
    nodes.Add(createdNode);

    //Trying to add sub-asset
    if (AssetDatabase.Contains(this))
    {
        Debug.Log("Dialogue Asset exists");
        AssetDatabase.AddObjectToAsset(createdNode, this);
    }
    else
    {
        Debug.Log("Dialogue Asset does not exist");
    }

    //Adding to the lookup
    OnValidate();

    return createdNode;
}

Then I created a new Dialogue SO.

Here is the console “while” creating the SO (i.e. as far as you are “cleverly giv[ing] the SO a name”):

And here is the console just right after you have finished naming the SO:

We can see that:

  • Awake is only run once (not a surprise)
  • A root node (82d0c…) is created (DialogueEditor.OnGUI called Dialogue.CreateNode)
  • The Dialogue’s asset hasn’t been created yet (seems normal because we are scratching our head about its name)
  • DialogueEditor.OnGUI get’s called another time, right after the name of the SO is set… and calls Dialogue.CreateNode another time… which means that
    if (selectedDialogue.GetAllNodes().FirstOrDefault() == null) is true… :scream:
  • A new root node has been created(0648d7c9…)
  • The corresponding asset has had plenty of time to get created in the AssetDatabase, so we can now create a sub asset

I feel like I understood what was going on and can now go to bed amidst a profound sense of fulfillment.

Once again: @Brian_Trotter, thank you very much for pointing the right direction!

Only when it’s created, as in when you create it via the AssetMenu. If you close the editor and restart, Awake() should actually find it’s empty and create one.

Privacy & Terms