Conversation System

Node Based Conversation System

A conversation in the game is a series of predefined prompts and the choice of answers. Each piece of NPC dialogue is accompanied by several options, each option leads to either another piece of dialogue or exits. A collection of dialogues is a conversation.
This is illustrated quite well in a class diagram of the conversation system.

  • A ConversationScreen is a view of a single Conversation.
  • A Conversation.Dialogue is a collection of ConversationNode.
  • ConversationNode.Options is a collection of ConversationOption.
  • ConversationOption.NodeId property refers to the next node to visit if the option is chosen.
  • Negative NodeId indicates an exit of the conversation.

conversation-class-Graph

Conversation Class

The conversation class is mostly a wrapper around a List<ConversationNode> and consists of methods to add/remove nodes and options. The class also serializes a conversation to an XML file, allowing quick swapping of dialogue and editing outside of the engine.
Update: To aid designers, and facilitate faster conversation creation, the class now immediately populates itself with the serialised file, and overwrites the file set in the inspector

[Serializable]
public class Conversation {
    /// <summary>
    ///     Container for ConversationNode's
    /// </summary>
    //[NonSerialized]
    public List<ConversationNode> Dialogue;

    /// <summary>
    ///     The node that the current conversation is on.
    /// </summary>
    [SerializeField, HideInInspector]
    private ConversationNode _currentNode;

    /// <summary>
    ///     The node that the current conversation is on.
    /// </summary>
    public ConversationNode CurrentNode { get { return _currentNode; } set { _currentNode = value; } }

    /// <summary>
    ///     Add a node to the conversation tree
    /// </summary>
    /// <param name="node">The node to add</param>
    public void AddNode(ConversationNode node) {
        if (node != null) {
            Dialogue.Add(node);
            node.NodeID = Dialogue.IndexOf(node);
        }
    }

    /// <summary>
    ///     Add an option to a Conversation Node
    /// </summary>
    /// <param name="node">The node to add the option to</param>
    /// <param name="destinationNode">The destination node for the option</param>
    /// <param name="text">The Text for the option</param>
    public void AddOption(ConversationNode node, ConversationNode destinationNode, string text) {
        if (!Dialogue.Contains(node)) {
            AddNode(node);
        }
        if (destinationNode == null) {
            node.Options.Add(new ConversationOption(-1, text));
            return;
        }

        if (!Dialogue.Contains(destinationNode)) {
            AddNode(destinationNode);
        }
        node.Options.Add(new ConversationOption(destinationNode.NodeID, text));
    }

    /// <summary>
    ///     Load a conversation from an xml file
    /// </summary>
    /// <param name="conversationFile"></param>
    public void LoadAssetFile(TextAsset conversationFile) {
        var x = new XmlSerializer(typeof(List<ConversationNode>));
        Dialogue = (List<ConversationNode>) x.Deserialize(new StringReader(conversationFile.text));
        Dialogue.Sort((a, b) => a.NodeID.CompareTo(b.NodeID));
        CurrentNode = Dialogue[0];
    }

    /// <summary>
    ///     Save a conversation to xml file
    /// </summary>
    public void SaveAssetFile() {
        var filename = "Conversation" + Random.Range(100000, 999999) + ".xml";
        var x = new XmlSerializer(typeof(List<ConversationNode>));
        using (TextWriter output = new StreamWriter(string.Format("{0}/Dialogue/{1}", Application.dataPath, filename))) {
            x.Serialize(output, Dialogue);
            output.Close();
        }
    }
}

Conversation node

/// <summary>
///     A node in the conversation tree
/// </summary>
[Serializable]
public class ConversationNode {
    /// <summary>
    ///     The ID of this node
    /// </summary>
    [PropertyOrder(0), BoxGroup]
    public int NodeID;

    /// <summary>
    ///     The list of options available
    /// </summary>
    [PropertyOrder(3), BoxGroup, Indent]
    public List<ConversationOption> Options;

    /// <summary>
    ///     The dialogue to display
    /// </summary>
    [TextArea, PropertyOrder(1), BoxGroup]
    public string TextData;
}

Option Node

/// <summary>
/// An Option in the conversation tree
/// </summary>
[Serializable]
public class ConversationOption {

    /// <summary>
    /// The ID of the exit node of this option
    /// </summary>
    public int DestinationNodeId;

    /// <summary>
    /// The dialogue of the option
    /// </summary>
    [TextArea]
    public string TextData;

    public ConversationOption(int destinationNodeId, string text) {
        TextData = text;
        DestinationNodeId = destinationNodeId;
    }

    public ConversationOption() { }
}

The conversation editor is rendered in the inspector as below, it serves as a way to create/edit a conversation file and automatically deserializes conversation content upon load.
conversation-inspector

Conversation Screen

I created a simple conversation screen manager class and UI to allow the player to carry out a conversation (screenshot below), navigating through the nodes with a click. It also features a scrolling history window.

conversation-screen

Future

I'd like to extend this by writing a node based editor similar to the mechanim animator built into unity, this would make task of editing a conversation much simpler for a designer.

Unity node graph