Copying a DialogueNode

One of the challenges with our DialogueEditor is copying an existing node. Sam has expertly shown us how to create a new node, to delete a node, and edit the links between nodes, but what about duplicating a node so you only have to make small adjustments to them?

One might think that you could go to the list of nodes in the regular inspector for a Dialogue and press the + key in the list of nodes. What you’ll find, however, is that this will simply add a new reference to the list of nodes, but it won’t actually create a new node itself. What we need to do to make this happen is to manually create a deep copy of the node.

There will be several moving parts for this, so let’s get started.

Requesting the Duplication

We'll start at the beginning, adding a button to each DialogueNode in the DialogueEditor to clone the node. This part's actually rather simple. If you navigate to the DialogueEditor's DrawNode() method, you'll see that we draw an X button to delete the node, an appropriate linking button, and a + button to Add a node. For the X and + buttons, we have a supporting deletingNode and creatingNode DialogueNode. So first things first, in our variable declarations at the top of the class, we need to add
DialogueNode duplicatingNode;

Now, at the end of the DrawNode() method, before ending the Horizontal section, we need to add a button that sets the duplicatingNode if pressed

        private void DrawNode(DialogueNode node)
        {
            GUIStyle style = nodeStyle;
            if (node.IsPlayerSpeaking())
            {
                style = playerNodeStyle;
            }
            GUILayout.BeginArea(node.GetRect(), style);
            EditorGUI.BeginChangeCheck();

            node.SetText(EditorGUILayout.TextField(node.GetText()));

            GUILayout.BeginHorizontal();

            if (GUILayout.Button("x"))
            {
                deletingNode = node;
            }
            DrawLinkButtons(node);
            if (GUILayout.Button("+"))
            {
                creatingNode = node;
            }
            //New Button # for duplicatingNode
            if (GUILayout.Button("#"))
            {
                duplicatingNode = node;
            }
            GUILayout.EndHorizontal();

            GUILayout.EndArea();
        }

Now with our new button in hand, we need to do something with it in OnGUI()

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

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

                EditorGUILayout.EndScrollView();

                if (creatingNode != null) 
                {
                    selectedDialogue.CreateNode(creatingNode);
                    creatingNode = null;
                }
                if (deletingNode != null)
                {
                    selectedDialogue.DeleteNode(deletingNode);
                    deletingNode = null;
                }
                //Check to see if this node should be duplicated.
                if (duplicatingNode != null)
                {
                    selectedDialogue.DuplicateNode(duplicatingNode);
                    duplicatingNode = null;
                }
            }
        }

Now here at the end, we’ve got a compilation error, because Dialogue does not have a method Duplicate Node.

Dialogue.cs, Duplicating the Node

We'll need to create a DuplicateNode() method in Dialogue. It's extremely important that this bit of code be within an #if UNITY_EDITOR block, as it will be referring to Editor Code at some parts.
#if UNITY_EDITOR
        DialogueNode FindParent(DialogueNode node)
        {
            DialogueNode parentNode;
            return nodes.FirstOrDefault(dialogueNode => dialogueNode.GetChildren().Contains(node.name));
        }
        
        public void DuplicateNode(DialogueNode duplicatingNode)
        {
            DialogueNode newNode = CreateNode(FindParent(duplicatingNode));
            newNode.Clone(duplicatingNode);
        }
#endif

So the first method is a utility to find the first parent of the node that we want to copy. This lets us create a new node with the same parent. Note that if you duplicate the root note, a new root node will be created, but under the current iteration of the system, it will simply be a dangling node and ignored.

So our DuplicateNode method Creates a new node using CreateNode() (which is also in and Editor Block). CreateNode takes care of all of the setup to put a new node within the Dialogue and take care of all that messy Undo creation logic. Once this is done, we want to clone the node.

What we can’t do is simply assign the new node to the duplicatingNode, as all this will do is make them share a link to the same object, and this is what we very much don’t want. So we’ll need to clone it. Probably not surprising, but Clone() does not exist DialogueNode.cs, so that’s next…

DialogueNode.Clone

This is where we get into the deep copy process. A deep copy means that you create a new copy of something rather than simply copying references... For example, Condition is a class... so if you just say that the new node's Condition is = to the old node's Condition, you'll just be pointing them to the same Condition... that's bad because the changes you make to one change the other (because they are both just *the one*.).

So let’s take a look. As you might suspect, we’ll be creating another .Clone method or two (ok, three) before we’re done. Also note that this method should also be within an #if UNITY_EDITOR block

        public void Clone(DialogueNode nodeToCopy)
        {
            isPlayerSpeaking = nodeToCopy.isPlayerSpeaking;
            text = nodeToCopy.text;
            rect = nodeToCopy.rect;
            children = nodeToCopy.children.ToList();
            rect.x += 50;
            rect.y += 50;
            onEnterAction = nodeToCopy.onEnterAction;
            onExitAction = nodeToCopy.onExitAction;
            condition = nodeToCopy.condition.Clone();
            EditorUtility.SetDirty(this);
        }

So up until the condition, what we’re really doing is simply copying line by line (and adding a slight offset to the node so it stands out.
Take a look at children. This line uses a System.Link method called ToList() to create a clean copy of the list, meaning that if the node you duplicate has children, you can unlink these children in one node without unlinking them in both nodes. (Try it with and without, and you’ll see).
Make sure you’ve added

using System.Linq;

to your usings clauses for this to work.

Of course, we also need to add a Clone() method to Condition. Condition is probably the most complex of our elements to deep copy, but it’s the one most people will be the most interested in making sure that we have a copy. You might set up all the conditions, and then copy the node just so you can click Negate on a condition in the new node rather than to create the whole thing again.

I’m going to paste in the whole Condition script, and you’ll see how in addition to Cloning the Condition, I also needed to clone each Disjunction within the Condition and each Predicate within the Disjunciton.

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

namespace GameDevTV.Utils
{
    [System.Serializable]
    public class Condition
    {
        [SerializeField]
        Disjunction[] and;

        public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
        {
            foreach (Disjunction dis in and)
            {
                if (!dis.Check(evaluators))
                {
                    return false;
                }
            }
            return true;
        }

        public Condition Clone()
        {
            Condition newCondition = new Condition();
            newCondition.and = new Disjunction[and.Length];
            for (int i = 0; i < and.Length; i++)
            {
                newCondition.and[i] = and[i].Clone();
            }
            return newCondition;
        }

        [System.Serializable]
        class Disjunction
        {
            [SerializeField]
            Predicate[] or;

            public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
            {
                foreach (Predicate pred in or)
                {
                    if (pred.Check(evaluators))
                    {
                        return true;
                    }
                }
                return false;
            }

            public Disjunction Clone()
            {
                Disjunction newDisjunction = new Disjunction();
                newDisjunction.or = new Predicate[or.Length];
                for (int i = 0; i < or.Length; i++)
                {
                    newDisjunction.or[i] = or[i].Clone();
                }
                return newDisjunction;
            }
        }

        [System.Serializable]
        class Predicate
        {
            [SerializeField]
            string predicate;
            [SerializeField]
            string[] parameters;
            [SerializeField]
            bool negate = false;

            public Predicate Clone()
            {
                Predicate newPredicate = new Predicate();
                newPredicate.predicate = predicate;
                newPredicate.parameters = parameters;
                newPredicate.negate = negate;
                return newPredicate;
            }
            
            public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
            {
                foreach (var evaluator in evaluators)
                {
                    bool? result = evaluator.Evaluate(predicate, parameters);
                    if (result == null)
                    {
                        continue;
                    }

                    if (result == negate) return false;
                }
                return true;
            }
        }
    }
}

And there you have it, you should now be able to press the # button to get a complete deep copy of any given node.

Privacy & Terms