XML Conversation System

This conversation system will be  large portion of the game.  Because there will be a large number of conversations throughout the game, I did not want to have to program a new script for each one.  This meant I needed a format to store all of this information, and a system that could display and reveal the information correctly. I decided to use XML for this storage method.  After writing a simple sample XML, I discovered that Unity has its own built in XML parser.  Once I made a script in Unity's C# that could parse my sample XML, I set out to plan a complete conversation the player may have.  

Example of the conversation tree.png

First I would need a method to store and later recall what I parsed.  I decided on a Hub and response system.  In a normal hub system each hub points to a different hub and the player will progress through all answers just by saying everything at least once. My system has a middle section called a response that can point to multiple hubs based on a multitude of variables.  These variables can range from what item they used at that moment to how the AI feels about them.  These responses will also store the Items and information the player will receive when they say the correct things.

Before even creating the complete conversation XML, I needed to decide how i was going to store this data.  I created an object for each of the Hub and the response so I can continually build functions and customize what the player experiences throughout their conversation.

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

public class Hub {

public int hubID;
public List<string> displayOptions;
public List<int> displayOptionLinks;
public List<string> NPCOptions;

public Hub(int tempHubID){

this.hubID = tempHubID;
this.NPCOptions = new List<string>();
this.displayOptions = new List<string>();
this.displayOptionLinks = new List<int>();

}

//Display Options a link along with what the player will say
public void AddDisplayOption(string newDisplay, int newDisplayLink){
displayOptions.Add(newDisplay);
displayOptionLinks.Add(newDisplayLink);

}

public void AddNPCOptions(string newOption){
NPCOptions.Add (newOption);
}

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

public class Response {

public int responseID;
public string normalResponse;
public int link;

public Response(int tempResponseID, string tempNormalResponse, int tempLink){

this.responseID = tempResponseID;
this.normalResponse = tempNormalResponse;
this.link = tempLink;
}

}

Once I knew how I wanted to store the data, it was just a matter of figuring out how Unity parses XML.  Unfortunately for me, when I started this project, I did not know that Unity had its own parser, so this section took me a bit longer.  The final script used to parse my XML is shown below.

//  <hubID> 0 </hubID>
// <NPCOptions>
// <firstTime> This is what the NPC says the first time you talk to them in main hub</firstTime>
// </NPCOptions>
// <options>
// <option>
// <normalWords> This is option 1 in main hub</normalWords>
// <link> 1 </link>
// </option>
// <option>
// <normalWords> This is option 2 in main hub</normalWords>
// <link> 2 </link>
// </option>
// <option>
// <normalWords> This is option 3 in main hub</normalWords>
// <link> 3 </link>
// </option>
// </options>
// </hub>
//<response>
// <responseID> 1 </responseID>
// <normalResponse> This is response 1</normalResponse>
// <link> 1 </link>
//</response>
//</conversation>
using UnityEngine;
using System.Collections.Generic;
using System.Xml;

public class LoadXMLFile {

public static XmlDocument mXmlDocument = new XmlDocument();
public static List<Hub> mLoadedHubs = new List<Hub>();
public static List<Response> mLoadedResponses = new List<Response>();

public static Hub currentLoadingHub;
public static string currentNormalWords;
public static int currentOptionLink;
public static int currentHubLink;

public static Response currentLoadingResponse;
public static int currentResponseID;
public static string currentNormalResponse;
public static int currentResponseLink;

public static void AssignXMLDoc(string importedXmlTextDoc){

mXmlDocument.LoadXml(importedXmlTextDoc);
ReadHubs(mXmlDocument.SelectNodes("conversation/hub"));
ReadResponses(mXmlDocument.SelectNodes("conversation/response"));
}

static void ReadHubs(XmlNodeList hubNodes){

foreach(XmlNode node in hubNodes){
currentLoadingHub = new Hub(XmlConvert.ToInt16(node.SelectSingleNode("hubID").InnerText));
currentLoadingHub.displayOptions = new List<string>();
currentLoadingHub.displayOptionLinks = new List<int>();
currentLoadingHub.NPCOptions = new List<string>();

foreach(XmlNode option in node.SelectNodes("options/option")){
currentNormalWords = option.SelectSingleNode("normalWords").InnerText;
currentOptionLink = XmlConvert.ToInt16(option.SelectSingleNode("link").InnerText);
currentLoadingHub.AddDisplayOption(currentNormalWords, currentOptionLink);

}

foreach (XmlNode NPCOptions in node.SelectNodes("NPCOptions")){
currentLoadingHub.AddNPCOptions(NPCOptions.InnerText);
}
mLoadedHubs.Add(currentLoadingHub);
}
}

static void ReadResponses(XmlNodeList responseNodes){

foreach(XmlNode node in responseNodes){
currentResponseID = XmlConvert.ToInt16(node.SelectSingleNode("responseID").InnerText);
currentNormalResponse = node.SelectSingleNode("normalResponse").InnerText;
currentResponseLink = XmlConvert.ToInt16(node.SelectSingleNode("link").InnerText);
currentLoadingResponse = new Response(currentResponseID, currentNormalResponse, currentResponseLink);
mLoadedResponses.Add (currentLoadingResponse);
}
}

}

Once this system was setup, I needed a way for the system to know what to display.  Also, I needed to display this information on the NGUI buttons and labels that will be stored at the camera for each conversation.  I first attempted to use a state machine, but that proved unnecessary for what I initially wanted to do.  I suspect I will be going back to one as this increases in complexity.  I created a "Conversation" script that will take an xml file, load it and then will coordinate with a Button Manager script to make sure the correct information is displayed.  These two scripts are displayed below.

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

public class Conversation : MonoBehaviour {

public TextAsset mXmlTextDocument;
public GameObject mConversationCamera;
public GameObject mGameManager;
public float mDisplayTime;
private float mDisplayTimeTracker;
private bool mIsHub;

private XmlDocument XmlDoc = new XmlDocument();
private List<Hub> mConversationHubs;
private List<Response> mConversationResponses;

//Prefabs for items recieved (need a more generic system later)
public GameObject mAlcoholPrefab;
public Inventory mInventory;

//How to track which Hub or Response to go to next
public int mCurrentLink = 0;


void Start() {

LoadXMLFile.AssignXMLDoc(mXmlTextDocument.text);
mConversationHubs = LoadXMLFile.mLoadedHubs;
mConversationResponses = LoadXMLFile.mLoadedResponses;

mConversationCamera.GetComponent<Conversation>().enabled = true;

mIsHub = true;
}

void Update() {

if(mIsHub){

this.GetComponent<ButtonManager>().UpdateHub(mConversationHubs[mCurrentLink]);

}
else{
if(!mIsHub){
DisplayResponse();
}
}
}

public void FindHubByID(int inputResponseLink){
for(int i = 0; mConversationHubs.Count > i; i++){
if(mConversationHubs[i].hubID == inputResponseLink){
mCurrentLink = i;
break;
}
}
mIsHub = true;
}

public void FindResponseByID(int inputHubLink){
for(int i = 0; mConversationResponses.Count > i; i++){
if(mConversationResponses[i].responseID == inputHubLink){
mCurrentLink = i;
break;
}
}

mIsHub = false;
}

void DisplayResponse(){

this.GetComponent<ButtonManager>().UpdateResponse(mConversationResponses[mCurrentLink]);
mDisplayTimeTracker += Time.deltaTime;

if(mDisplayTimeTracker >= mDisplayTime){

FindHubByID(mConversationResponses[mCurrentLink].link);
mDisplayTimeTracker = 0;
}
}
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ButtonManager:MonoBehaviour {


public GameObject mLabelLocation;
public GameObject[] mButtonLocations;

private Hub mCurrentHub;
private Response mCurrentResponse;
private bool mIfHub;

void Update(){

if(mIfHub){
CheckForButtonClicks();
}

}

public void UpdateHub(Hub importedHub){

mCurrentHub = importedHub;
mIfHub = true;
ChangePanelLabel();
ChangeButtonLabels(true);

}

public void UpdateResponse(Response importedResponse){

mCurrentResponse = importedResponse;
ChangeButtonLabels(false);
mIfHub = false;
ChangePanelLabel();

}

void ChangePanelLabel(){

if(mIfHub){
mLabelLocation.GetComponent<UILabel>().text = mCurrentHub.NPCOptions[0];
}
else{
mLabelLocation.GetComponent<UILabel>().text = mCurrentResponse.normalResponse;
}

}

void ChangeButtonLabels(bool buttonsOn){

if(buttonsOn){

int numberOfButtons = mCurrentHub.displayOptions.Count;

for(int i = 0; i<mButtonLocations.Length;i++){
mButtonLocations[i].SetActive(true);
}

for(int i = 0; i<numberOfButtons; i++){
mButtonLocations[i].GetComponentInChildren<UILabel>().text = mCurrentHub.displayOptions[i];
}
if(numberOfButtons < mButtonLocations.Length){
for(int i = numberOfButtons; i<mButtonLocations.Length;i++){
mButtonLocations[i].SetActive(false);
}
}
}
else{
for(int i = 0; i<mButtonLocations.Length;i++){
mButtonLocations[i].SetActive(false);
}
}
}

void CheckForButtonClicks(){

for(int i = 0; i< mButtonLocations.Length; i++){
UIEventListener.Get(mButtonLocations[i]).onClick += ButtonClicked;
}
}

void ButtonClicked(GameObject button){

for(int i = 0; i < mCurrentHub.displayOptions.Count; i++){
if(mCurrentHub.displayOptions[i] == button.GetComponentInChildren<UILabel>().text){
gameObject.GetComponent<Conversation>().FindResponseByID(mCurrentHub.displayOptionLinks[i]);

break;
}
}
}
}

This system was probably the most time consuming and challenging task I have completed thus far for my Investigative Adventure.  It will be revisited as I implement more of the features that I believe will make for an engaging system.  Feel free to contact me about this project with any questions.