Writing XML Log Files in Unity 3D using C#

Update

The code for this project has seen extensive changes and has since been migrated to GitHub. Read more about it over here – and then go forth and fork it. It is released under a Creative Commons License, so you are free to build upon it.

Since I want to make some basic statistics for my game at Fantoche, I needed some basic logging function of the player’s position.

Of course, this could also be done using a simple CSV file, but the perfectionist in me insisted on an XML format. A preliminary test showed me, that I would be able to transform the XML to a CSV later on, so that my S. O. would be able to use it in his own programs.1

Having set up my development environment in MonoDevelop, I started to work on the problem on how to get my data into a well-formed XML representation and onto the hard disk.

The first approach was to use serialisation. I created a Location class, with all the necessary attributes – until I realised that this would only allow me to get one dataset into a file. I wouldn’t be able to add more data to the file.

Conclusion: Serialisation is only good when you have one clearly defined object you want to dump onto the drive as a well-formed XML file.

So I tried to work with the XMLWriter. This seemed more promising, but again I ran into problems: Since I wanted to write some meta data at the beginning of the file, the writer stream would somehow span over several methods … which was not really working – it wrote the meta data, and then closed the file again.

Finally, I found some help online at Microsoft on modifying large XML files:

The first technique I’ll demonstrate is targeted at situations where the goal is to be able to quickly append entries to an XML document. This approach involves creating two files. The first file is a well-formed XML file while the second is an XML fragment. The well-formed XML file includes the XML fragment using either an external entity declared in a DTD or using an xi:include element. This way the file containing the XML fragment can be updated efficiently by simply appending to it while processing is done using the including file.

This is now exactly what I did. The following code will write the wrapper XML file as well as prepare for the fragment:

  1. using UnityEngine;
  2. using System.Collections;
  3. using System.IO;
  4. using System;
  5. using System.Xml;
  6.  
  7. public class SaveLocation : MonoBehaviour
  8. {
  9.  
  10.    public GameObject trackedPlayer;
  11.    public GameObject calibration;
  12.  private XmlWriterSettings fragmentSettings;
  13.     private string logFilePath;
  14.     FileStream logfile;
  15.  
  16.    // Use this for initialization
  17.  void Start ()
  18.   {
  19.       XmlWriterSettings wrapperSettings = new XmlWriterSettings ();
  20.       wrapperSettings.Indent = true;
  21.      string basename = DateTime.Now.ToOADate ().ToString ();
  22.         string wrappername = "tracker/wrapper-" + basename + ".xml";
  23.        logFilePath = "tracker/log-" + basename + ".xml";
  24.  
  25.      trackedPlayer = GameObject.FindGameObjectWithTag ("Player");
  26.  
  27.         // Write the wrapper file
  28.       GameObject ld = GameObject.Find ("LevelData");
  29.        string version = ld.GetComponent<LevelData> ().levelVersion;
  30.      string doctype = "<!DOCTYPE trackerdata \n [ \n <!ENTITY locations SYSTEM \"log-" + basename + ".xml\">\n ]>";
  31.  
  32.       using (XmlWriter writer = XmlWriter.Create (wrappername, wrapperSettings)) {
  33.            writer.WriteStartDocument();
  34.            writer.WriteRaw (doctype);  
  35.            writer.WriteStartElement ("trackerdata");
  36.  
  37.            // meta
  38.             writer.WriteStartElement ("meta");
  39.  
  40.  
  41.          writer.WriteStartElement ("starttime");
  42.           writer.WriteValue (DateTime.Now);
  43.           writer.WriteEndElement ();
  44.  
  45.             writer.WriteElementString ("levelversion", version);
  46.  
  47.             writer.WriteEndElement ();
  48.          // /meta
  49.  
  50.           writer.WriteStartElement ("tracking");
  51.            writer.WriteEntityRef ("locations");          
  52.            writer.Close ();
  53.        }
  54.  
  55.      // Prepare the log XML fragment
  56.         logfile = new FileStream (logFilePath, FileMode.Append, FileAccess.Write, FileShare.Read);
  57.  
  58.         fragmentSettings = new XmlWriterSettings ();
  59.        fragmentSettings.ConformanceLevel = ConformanceLevel.Fragment;
  60.      fragmentSettings.Indent = true;
  61.         fragmentSettings.OmitXmlDeclaration = false;
  62.  
  63.       InvokeRepeating("CollectData", 0, 5);
  64.     }

The previous code will produce an XML file along the lines of this:

  1. <?xml version="1.0" encoding="utf-8"?><!DOCTYPE trackerdata 
  2.  [ 
  3.  <!ENTITY locations SYSTEM "log-40309.7757512582.xml">
  4.  ]>
  5. <trackerdata>
  6.   <meta>
  7.     <starttime>2010-05-11T18:37:04.9179390+02:00</starttime>
  8.     <levelversion>0</levelversion>
  9.   </meta>
  10.   <tracking>&locations;</tracking>
  11. </trackerdata>

The function CollectData() being called looks like this:

  1. void CollectData ()
  2.    {
  3.  
  4.      using (XmlWriter writer = XmlWriter.Create (logfile, fragmentSettings)) {
  5.           writer.WriteStartElement ("location");
  6.  
  7.           writer.WriteStartAttribute ("runningTime");
  8.           writer.WriteValue (Time.timeSinceLevelLoad);
  9.            writer.WriteEndAttribute ();
  10.  
  11.           writer.WriteStartElement ("x");
  12.           writer.WriteValue (trackedPlayer.transform.position.x);
  13.             writer.WriteEndElement ();
  14.  
  15.             writer.WriteStartElement ("y");
  16.           writer.WriteValue (trackedPlayer.transform.position.y);
  17.             writer.WriteEndElement ();
  18.  
  19.             writer.WriteStartElement ("z");
  20.           writer.WriteValue (trackedPlayer.transform.position.z);
  21.             writer.WriteEndElement ();
  22.  
  23.             writer.WriteEndElement ();
  24.          writer.Flush ();
  25.  
  26.       }
  27.  
  28.      logfile.Flush ();
  29.   }

This function produces the following output:

  1. <location runningTime="0">
  2.   <x>-5.080078</x>
  3.   <y>-23.35105</y>
  4.   <z>0.6675512</z>
  5. </location>
  6. <location runningTime="4">
  7.   <x>-5.080078</x>
  8.   <y>-23.35105</y>
  9.   <z>0.6675512</z>
  10. </location>
  11. <location runningTime="9">
  12.   <x>-5.080078</x>
  13.   <y>-23.35105</y>
  14.   <z>0.6675512</z>
  15. </location>

The script does more or less what it is supposed to do. Still on the list of desirable features are:

  • The ability to call CollectData() at any time, and passing a string why it was called (used to track special occasions, like the death of the avatar or reaching a trigger). This could be done by method overloading.

In hindsight, using XMLDocument might have been wiser, but since I didn’t know about that possibility when writing the script, I solved it with the XMLWriter.


  1. So why again am I doing it with XML? Good question. Because I can? Does that make me a nerd? 

Social Tags:
XML

Neuen Kommentar schreiben