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

Comments

Note that you can’t just use any path to save your file if you’re also targeting iOS and Android.

I just made a blog post explaining how to obtain a valid file path in a platform-independent way (working on both PC, Mac, iOS and Android): http://www. previewlabs.com/file-io-in-unity3d/

Thanks, I was not aware of that. At the time of creating the code, the target platform was limited to Mac, so I was able to get away with it. Since I made a github repo of the project, I’ll better update the code to reflect your improvement.

Thanks man! Exactly what i needed!

Hi, it works well in editor, but I get MethodAccessException  Attempt to Acess a provate/protected method failed.

when I run from web player build

Thanks in Advance!!

I never tested it in the WebPlayer. It is possible that some methods aren’t available in WebPlayer builds, since they don’t contain the complete .NET/Mono framework, as documented here.

Hi, sorry if this is completely stupid question but where does this save the created xml file? I Tried to run my game with it but I don’t seem to find the created file from anywhere. :o

Uuhm … good question.

To be honest, I’ve run that code 5 years ago, so I don’t exactly remember by setup I used then.

Out of the box and using the version found on GitHub, it should be in a directory <companyname>/<gamename>/tracker, where companyname and gamename correspond to the variables in your Player Settings. But where this directory is located, I have no idea anymore … Probably relative to the game itself?

Try changing the directory variable in SaveLocation.cs, that might help :)

Hi, thank you for your answer. Adding a script that checks if the directory exists or not is definitely a good idea! ) I don’t know if I was sleeping yesterday or something but it seems that today I was able to find the files, weird! I was also troubled what happens when I save the game to other computer when I’m only saving the Builds folder. I created folder called “Logs” into the Builds folder and named the logFilePath as “Logs/log-” + myOwnStuffHere + “.xml” and it works great now.

And thank you for your post anyway, it was really useful for me and I don’t think anyone so far has made this so easy to understand!

If I remember correctly this game object is used mark the zero point. Most of the time you just need an empty game object at the 0/0/0 position. I think this was needed for statistics purposes, and could be repurposed to build a visualisation directly in Unity.

Add new comment