PropertyListLoader obsolete

From Unify Community Wiki
Revision as of 07:01, 23 February 2009 by Capnbishop (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Author: capnbishop


The PropertyListLoader.js script is used to load a plist file into a hashtable. This can provide a convenient and dynamic means of storing a complex hierarchy of game data into XML files.

The resulting hashtable can include 8 different types of values: string, integer, real, date, data, boolean, dictionary, and array. Data elements are loaded as strings. Dictionaries are loaded as hashtables. Arrays are loaded as arrays. Each value is loaded with an associating key, except for elements of an array. Thus, each child hashtable and array also have associating keys, and can be combined to create a complex hierarchy of key value pairs and arrays.

This script passes a lot of values by reference, and performs a considerable amount of recursion. Primitive values had to be passed by reference. UnityScript only passes objects by reference, and cannot explicitly pass a primitive by reference. As such, we've had to create a special ValueObject, which is just an abstract object that holds a single value. This object is then passed by reference, and the primitive value is set to its val property.

This plist loader conforms to Apple's plist DOCTYPE definition:


To use this script, call the LoadPlistFromFile(String, Hashtable) function and pass it the path to a plist file and a hashtable that will be populated with the plist data. LoadPlistFromFile(String, Hashtable) will return true if the operation was successful. The plist passed must not be null, as it has to be passed by reference.

The easiest way to create a plist is with Apple's Property List Editor, which is a part of the Xcode developer tools.

LoadPlistFromFile() needs to be passed an already instantiated hashtable, because that hashtable is passed by reference. Because of this, it is also able to be passed an already populated plist hashtable. This is fine, and the script will mesh the two hashtables by overwriting the values of existing keys along the hashtable tree structure. For sub-hashtables, the overwriting will follow the structure, and only overwrite the existing keys within the sub-hashtables. Essentially, sub-hashtables themselves aren't overwritten, but the key/value pairs within them can be. Elements of arrays are simply appended to the existing array, and not overwritten at all.

<javascript>import System; import System.IO;

function Start () {

   var xmlFiles = Directory.GetFiles((Application.dataPath), "*.plist");
   if (xmlFiles.length < 1) { Debug.Log("Unable to find any plist files."); }
   else {
       var plist = new Hashtable();
       if (PropertyList.LoadPlistFromFile(xmlFiles[0], plist)) {
           for (var i in plist.Keys) {
       else { Debug.Log("Unable to open plist file."); }


Javascript - PropertyListLoader.js

<javascript>// We use the .Net XML and DateTime libraries in this script import System; import System.IO; import System.Xml; import System.DateTime; import System.Globalization;

// We need to be able to pass values by reference fluidly, including primatives. UnityScript doesn't support passing non objects by reference. Thus, we've had to create an object that simple stores a single value. This way, we can pass the ValueObject, and just use its val value. class ValueObject { function ValueObject(aVal) { val = aVal; } function ValueObject() {} var val; };

// LoadPlistFromFile(String, Hashtable) is the root public function for loading a plist file into memory. The plist is loaded into the hashtable passed. Return true/false for success/failure static public function LoadPlistFromFile(xmlFile : String, plist : Hashtable) : boolean {

   // Unless plist has already been initiated, it can't be passed by reference, which it has to be
   if (!plist) { Debug.LogError("Cannot pass null plist value by reference to LoadPlistFromFile."); }
   // If the file doesn't exist, return a false
   if (!File.Exists(xmlFile)) { Debug.LogError("File doesn't exist: " + xmlFile); return false; }
   // Load the file into an XML data object
   var sr = new StreamReader(xmlFile);
   var txt = sr.ReadToEnd();
   var xml = new XmlDocument();
   // Find the root plist object.  If it doesn't exist or isn't a plist node, state so and return null.
   var plistNode = xml.LastChild;
   if (plistNode.Name != "plist") { Debug.LogError("This is not a plist file: " + xmlFile); return false; }
   // Get the version number of this plist file.  This script was designed to work with version 1.0.  If this is an incorrect version, state so and return null
   var plistVers = plistNode.Attributes["version"].Value;
   var plistVersSupported = "1.0";
   if (plistVers != plistVersSupported) { Debug.LogError("This is an unsupported plist version: " + plistVers + ". Require version " + plistVersSupported); return false; }
   // Get the root plist dict object.  This is the root object that contains all the data in the plist file.  This object will be the hashtable.
   var dictNode = plistNode.FirstChild;
   if (dictNode.Name != "dict") { Debug.LogError("Missing root dict from plist file: " + xmlFile); return false; }
   // Using the root dict node, load the plist into a hashtable and return the result.
   // If successful, this will return true, and the plist object will be populated with all the appropriate information.
   return LoadDictFromPlistNode(dictNode, plist);


// LoadDictFromPlistNode(XmlNode, Hashtable) takes an XML node and loads it as a hashtable. Return true/false for success/failure static private function LoadDictFromPlistNode(node : XmlNode, dict : Hashtable) : boolean {

   // If we were passed a null object, return false
   if (!node) { Debug.LogError("Attempted to load a null plist dict node."); return false; }
   // If we were passed a non dict node, then post an error stating so and return false
   if (node.Name != "dict") { Debug.LogError("Attempted to load an dict from a non-array node type: " + node + ", " + node.Name); return false; }
   // We can be passed an null hashtable.  If so, initialize it.
   if (!dict) { dict = new Hashtable(); }
   // Identify how many child nodes there are in this dict element and itterate through them.
   // A dict element will contain a series of key/value pairs.  As such, we're going through the child nodes in pairs.
   var cnodeCount = node.ChildNodes.Count;
   for (var i = 0; i+1 < cnodeCount; i = i+2) {
       // Select the key and value child nodes
       var keynode = node.ChildNodes.Item(i);
       var valuenode = node.ChildNodes.Item(i+1);
       // If this node isn't a 'key'
       if (keynode.Name == "key") {
           // Establish our variables to hold the key and value.
           var key = keynode.InnerText;
           var value : ValueObject = new ValueObject();
           // Load the value node.
           // If the value node loaded successfully, add the key/value pair to the dict hashtable.
           if (LoadValueFromPlistNode(valuenode, value)) {
               // This could be one of several different possible data types, including another dict.
               // AddKeyValueToDict() handles this by replacing existing key values that overlap, and doing so recursively for dict values.
               // If this not successful, post a message stating so and return false.
               if (!AddKeyValueToDict(dict, key, value)) { Debug.LogError("Failed to add key value to dict when loading plist from dict"); return false; }
           // If the value did not load correctly, post a message stating so and return false.
           else { Debug.LogError("Did not load plist value correctly for key in node: " + key + ", " + node); return false; }
       // Because the plist was formatted incorrectly, post a message stating so and return false.
       else { Debug.LogError("The plist being loaded may be corrupt."); return false; }
   // If we got this far, the dict was loaded successfully.  Return true
   return true;


// LoadValueFromPlistNode(XmlNode, Object) takes an XML node and loads its value into the passed value object. // The value for this node can be one of several different possible types. Return true/false for success/failure static private function LoadValueFromPlistNode(node : XmlNode, value : ValueObject) : boolean {

   // If passed a null node, post an error stating so and return false
   if (!node) { Debug.LogError("Attempted to load a null plist value node."); return false; }
   // Identify the data type for the value node and assign it accordingly
   if      (node.Name == "string")   { value.val = node.InnerText; }
   else if (node.Name == "integer")  { value.val = parseInt(node.InnerText); }
   else if (node.Name == "real")     { value.val = parseFloat(node.InnerText); }
   else if (node.Name == "date")     { value.val = DateTime.Parse(node.InnerText, null, DateTimeStyles.None); } // Date objects are in ISO 8601 format
   else if (node.Name == "data")     { value.val = node.InnerText; } // Data objects are just loaded as a string
   else if (node.Name == "true")     { value.val = true; } // Boollean values are empty objects, simply identified with a name being "true" or "false"
   else if (node.Name == "false")    { value.val = false; }
   // The value can be an array or dict type.  In this case, we need to recursively call the appropriate loader functions for dict and arrays.
   // These functions will in turn return a boolean value for their success, so we can just return that.
   // The val value also has to be instantiated, since it's being passed by reference.
   else if (node.Name == "dict")     { value.val = new Hashtable(); return LoadDictFromPlistNode(node, value.val); }
   else if (node.Name == "array")    { value.val = new Array(); return LoadArrayFromPlistNode(node, value.val); }
   else                              { Debug.LogError("Attempted to load a value from a non value type node: " + node + ", " + node.Name); return false; }
   // If we made it this far, then we had success.  Return true.
   return true;


// LoadArrayFromPlistNode(XmlNode, Array) takes an XML node and loads it as an Array. A plist array is just a series of value objects without keys. static private function LoadArrayFromPlistNode(node : XmlNode, array : Array) : boolean {

   // If we were passed a null node object, then post an error stating so and return false
   if (!node) { Debug.LogError("Attempted to load a null plist array node."); return false; }
   // If we were passed a non array node, then post an error stating so and return false
   if (node.Name != "array") { Debug.LogError("Attempted to load an array from a non-array node type: " + node + ", " + node.Name); return false; }
   // We can be passed an empty array object.  If so, initialize it
   if (!array) { array = new Array(); }
   // Itterate through the child nodes for this array object
   var nodeCount = node.ChildNodes.Count;
   for (var i = 0; i < nodeCount; i++) {
       // Establish variables to hold the child node of the array, and it's value
       var cnode = node.ChildNodes.Item(i);
       var element = new ValueObject();
       // Attempt to load the value from the current array node.
       // If successful, add it as an element of the array.  If not, post and error stating so and return false.
       if (LoadValueFromPlistNode(cnode, element)) { array.Add(element.val); }
       else { return false; }
   // If we made it through the array without errors, return true
   return true;


// AddKeyValueToDict(Hashtable, String, Object) handles adding new or existing values to a hashtable. // A hashtable can already contain the key that we're trying to add. If it's a regular value, we can just replace the existing one with the new one. // If trying to add a hashtable value to another hashtable that already has a hashtable for that key, then we need to recursively add new values. // This allows us to load two plists with overlapping values so that they will be combined into one big plist hashtable, and the new values will replace the old ones with the same keys static private function AddKeyValueToDict(dict : Hashtable, key : String, value : ValueObject) : boolean {

   // Make sure that we have values that we can work with.
   //if (!dict || !key || key == "" || !value || !value.val) { Debug.LogError("Attempted to AddKeyValueToDict() with null objects."); return false; }
   // If the hashtabel doesn't already contain the key, they we can just go ahead and add it.
   if (!dict.ContainsKey(key)) { dict.Add(key, value.val); return true; }
   // At this point, the dict contains already contains the key we're trying to add.
   // If the value for this key is of a different type between the dict and the new value, then we have a type mismatch.
   // Post an error stating so, but go ahead and overwrite the existing key value.
   if (typeof(value.val) != typeof(dict[key])) {
       Debug.LogWarning("Value type mismatch for overlapping key (will replace old value with new one): " + value.val + ", " + dict[key] + ", " + key);
       dict[key] = value.val;
   // If the value for this key is a hashtable, then we need to recursively add the key values of each hashtable.
   else if (typeof(value.val) == Hashtable) {
       // Itterate through the elements of the value's hashtable.
       for (element in value.val.Keys) {
           // Recursively attempt to add/repalce the elements of the value hashtable to the dict's value hashtable.
           // If this fails, post a message stating so and return false.
           if (!AddKeyValueToDict(dict[key], element, new ValueObject(value.val[element]))) {
               Debug.LogError("Failed to add key value to dict: " + element + ", " + value.val[element] + ", " + dict[key]);
               return false;
   // If the value is an array, then there's really no way we can tell which elements to overwrite, because this is done based on the congruent keys.
   // Thus, we'll just add the elements of the array to the existing array.
   else if (typeof(value.val) == Array) {
       for (element in value.val) { dict[key].Add(element); }
   // If the key value is not an array or a hashtable, then it's a primitive value that we can easily write over.
   else { dict[key] = value.val; }
   // If we've gotten this far, then we were successful.  Return true.
   return true;


Personal tools