Unity and XML

Start Parsing XML Data for Your Games

Posted by Aaron Salisbury on August 30, 2015

For my text adventure game, A Winter's Tale, I soon realized I had a lot of text and repetitive objects in each of my scenes. I thought about how to best organize and deliver it and then I remembered good-ol-XML. I originally had a separate scene in Unity for each part of the story, but every scene was essentially the same; an image, story text, and a number of buttons for the player's choice of action. Also in the back of my mind I was contemplating the caching of those images so each wouldn't have to load on every scene. Why not, then, create scene elements by name in XML that house this data since each scene has these similar characteristics?

XML 101

XML is easy to read and self-described. If you've worked with HTML, then you've essentially worked with a subset of XML. Infact, XML is more flexible as there are no predefined tags. In this way, XML is tailored to organizing your data as you see fit.


<?xml version="1.0" encoding="utf-8" ?>

<scenes>
    <scene name="Scene_Street" image="Scene_Street" audioClip="">
        <story>You awake and rise in the narrow back alley that you call home.</story>
        <options>
            <option loads="Scene_Village-Center">{1} Go to town center.</option>
            <option loads="Scene_Village-Door">{2} Go to door.</option>
            <option loads="Scene_Bridge">{3} Leave town.</option>
            <option loads="Scene_Death_Sleep">{4} Go back to sleep.</option>
        </options>
    </scene>
    <scene name="Scene_Dragon" image="Scene_Dragon" audioClip="boss-fight">
        <story>You're attacked by a White Dragon!</story>
        <options>
            <option loads="Scene_Death_Dragon_Attack">{1} Attack!</option>
            <option loads="Scene_Tower">{2} Shield.</option>
            <option loads="Scene_Death_Dragon_Run">{3} Run.</option>
        </options>
    </scene>
</scenes>

In the sample shown above, I'm indicating that this is a collection of "scenes" and nested inside that can be any number of "scene" children. Attributes of the scene include name, image, and audioClip in case I'd like one to play upon load. Sibling elements of the scene include the story itself and a number of options. In code I assume a minimum of one option and a maximum of four. These correspond directly to the buttons on screen, which render conditionally based on the presence of these options. I could have named these elements and attributes anything I liked.

Preparing the Script

In Unity I added an empty game object to the scene and attached a single C# script to it. The game is simple enough that this single class can control all the gameplay, so I don't have to worry about persisting that XML data across multiple scenes and scope. At the top of the script, include the System.Xml using statement. Now days Linq to XML is the favored parsing technology, but the support for it by the Mono compiler is shakey. I did get it to work by manually copying the DLL into my project, but it was always buggy and drove me crazy, so in this case, the old ways are better. Just means we'll have to brush up on our XPath.


Sprite[] sceneSprites = Resources.LoadAll<Sprite>("SceneSprites");
Dictionary<string, Sprite> sceneImagesByName = new Dictionary<string, Sprite>();

for (int i = 0; i < sceneSprites.Length; i++)
{
    sceneImagesByName[sceneSprites[i].name] = sceneSprites[i];
}

AudioClip[] sceneAudioClips = Resources.LoadAll<AudioClip>("SceneAudioClips");
Dictionary<string, AudioClip> sceneAudioClipsByName = new Dictionary<string, AudioClip>();

for (int i = 0; i < sceneAudioClips.Length; i++)
{
    sceneAudioClipsByName[sceneAudioClips[i].name] = sceneAudioClips[i];
}

From the Unity start method, I call my parsing method. I created a dictionary to store all my image and audio files by name. The files must be located in Assets > Resources and the string parameter passed to the LoadAll method is the name of the folder the given files are actually in.


TextAsset textAsset = (TextAsset)Resources.Load("SceneText");
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml(textAsset.text);

Next I similarly bring in the XML file itself, named "SceneText", from the Resources folder and cast it as a TextAsset. Then I can use that text to load my XmlDocument object.


private class LevelData
{
    public Sprite LevelImage;
    public AudioClip LevelAudioClip;
    public float Volume = 0.1f;
    public string LevelStory;
    public string AlternateLevelStory;
    public Dictionary<string, string> LevelButtonsAndLoads;
    public Dictionary<string, string> AlternateLevelButtonsAndLoads;
}

Before getting into the real work of parsing that XmlDocument, let me show you my setup. I created a nested class called LevelData to house the individual pieces needed for each level, and then created a class level collection to store that LevelData by name of the level. So now at any point I can just get the name of the level I'm on and have instant in-memory access to all elements.

Parsing With C#


TextAsset textAsset = (TextAsset)Resources.Load("SceneText");
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml(textAsset.text);

foreach (XmlNode scene in xmlDocument["scenes"].ChildNodes)
{
    string sceneName = scene.Attributes["name"].Value;

    levelDataBySceneName[sceneName] = new LevelData() 
    {
        LevelImage = sceneImagesByName[scene.Attributes["image"].Value],
        LevelAudioClip = null,
        LevelStory = scene["story"].InnerText,
        AlternateLevelStory = scene["alternateStory"].InnerText,
        LevelButtonsAndLoads = new Dictionary<string,string>(),
        AlternateLevelButtonsAndLoads = new Dictionary<string,string>()
    };

    if (!string.IsNullOrEmpty(scene.Attributes["audioClip"].Value))
    {
        levelDataBySceneName[sceneName].LevelAudioClip = sceneAudioClipsByName[scene.Attributes["audioClip"].Value];
    }

    foreach(XmlNode option in scene["options"].ChildNodes)
    {
        levelDataBySceneName[sceneName].LevelButtonsAndLoads[option.InnerText] = option.Attributes["loads"].Value;
    }

    foreach (XmlNode alternateOption in scene["alternateOptions"].ChildNodes)
    {
        levelDataBySceneName[sceneName].AlternateLevelButtonsAndLoads[alternateOption.InnerText] = alternateOption.Attributes["loads"].Value;
    }
}

In the above code, after initializing my XmlObject I begin looping across the children of my "scenes" element. I get the value of the attribute named "name", and use that as the key of my dictionary. Finally I set the LevelData fields using a combination of those techniques.

Rendering to the Unity Scene

Next I'll go over some of how I use this collection to populate my scene.


// Gameplay Variables
private string currentLevelName;
private string lastPlayedLevelName;
private Button[] currentLevelChoiceButtons;
private string[] levelsThatCurrentButtonsLoad;

// Level Objects
public Image ImageStory;
public Text TextStory;
public Button BtnChoiceOne;
public Button BtnChoiceTwo;
public Button BtnChoiceThree;
public Button BtnChoiceFour;


currentLevelChoiceButtons = new Button[] { null, BtnChoiceOne, BtnChoiceTwo, BtnChoiceThree, BtnChoiceFour };
levelsThatCurrentButtonsLoad = new string[5];
int buttonNumber = 1;

TextStory.text = levelDataBySceneName[sceneName].LevelStory;

foreach (KeyValuePair<string, string> buttonAndLoad in levelDataBySceneName[sceneName].LevelButtonsAndLoads)
{
    currentLevelChoiceButtons[buttonNumber].GetComponent<Text>().text = buttonAndLoad.Key;
    currentLevelChoiceButtons[buttonNumber].interactable = true;
    levelsThatCurrentButtonsLoad[buttonNumber] = buttonAndLoad.Value;

    buttonNumber++;
}

Again I leverage class level variables, sending in my image, text, and button controls from the Unity editor. The I initialize a Button array with those buttons and set their values. Notice, looping over the LevelButtonsAndLoads is really looping over those option elements from the XML. If there was one option, then there will be one loop iteration, and so on.


// Disable unused Buttons. Assumes minimum of one Button and maximum of four Buttons.
for (int i = buttonNumber; i <= 4; i++)
{
    currentLevelChoiceButtons[i].GetComponent<Text>().text = "";
    currentLevelChoiceButtons[i].interactable = false;
}

Finally, if the number of option/buttons given to me from the XML is less than 4, then disable those extra ones from the scene.

Check out the finished product HERE.

I hope that I've helped some people or maybe given you something to think about. Good coding!