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 singleConversation
. - A
Conversation.Dialogue
is a collection ofConversationNode
. ConversationNode.Options
is a collection ofConversationOption
.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
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 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.
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.