Saturday, February 27, 2010

Simple Settings Manager for XNA

Being able to load settings into a game is a must. There are a number of ways to accomplish this. My current method works well enough for now, although it needs a bit of work in a few areas. I accomplish this by creating a series of settings classes that can be serialized into XML and back again at run time using my simple serializer.

Settings Manager

public class SettingsManager
{
    const string Extension = ".xml";
    const string Directory = "Content\\Settings\\";

    /// <summary>
    /// Loaded settings - Not currently implemented
    /// </summary>
    //SerializableDictionary<string, SettingsBase> _settings;

    /// <summary>
    /// Constructor
    /// </summary>
    public SettingsManager()
    {
    }

    /// <summary>
    /// Load a settings file
    /// </summary>
    public T Load<T>()
    {
        Type t = typeof(T);
        return Load<T>(t.Name);
    }

    /// <summary>
    /// Attempt to load a settings file.  If not found, it will
    /// load a new, default instance of the settings class.
    /// </summary>
    public T Load<T>(string name)
    {
        try
        {
            return Serializer.Read<T>(
                SettingsManager.Directory + 
                name +
                SettingsManager.Extension);
        }
        catch (Exception e)
        {
            e = null;
            Type t = typeof(T);
            return (T)Activator.CreateInstance(t);
        }
    }

    /// <summary>
    /// Save a settings object
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public void Save<T>(T o)
    {
        Type t = typeof(T);
        Save<T>(o, t.Name);
    }

    /// <summary>
    /// Save a settings object
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="file_name"></param>
    public void Save<T>(T o, string name)
    {
        Serializer.Write<T>(
            o,
            SettingsManager.Directory + name + SettingsManager.Extension);
    }
}

The basic idea is that we can load and save settings files either based on an arbitrary name or, for simplicity, the settings class Type.Name. This is still a rough class - it assumes a lot about the settings files being loaded such as the files being serializable. I currently have a simple class that settings files inherit from:

Settings Base

[Serializable]
public abstract class SettingsBase
{
}

More functionality could be added here such as a Save() function that allowed settings objects to save themselves via the SettingsManager. The only special thing in this class is adding the [Serializable] tag at the top to ensure that all inheriting objects get this.

Simple Settings Example

public class PlayerSettings : SettingsBase
{
    public int Id;
    public string Name;

    public PlayerSettings()
    {
        Id = 1;
        Name = "Spartacus";
    }
}

Now, the settings manager can be used to load/save this.

// Load default settings
PlayerSettings ps = settingsManager.Load<PlayerSettings>();

// Change something
ps.Name = "Hercules";

// Save with default name
settingsManager.Save<PlayerSettings>(ps);

Future Improvements

  • SettingsManager does not ensure that loaded files inherit from SettingsBase at all.
  • SettingsManager does not track previously loaded settings files. Tracking and storing loaded files would prevent costly file IO, deserialization and reflection.
  • Settings classes assume that the constructor will set the default settings.

Friday, February 19, 2010

Reading XML Feeds in PHP

PHP has the super simple, and indeed aptly named SimpleXMLElement class.  Give it a string or a url and voila, parsed XML. Except, wait, the arch-nemesis of native PHP objects looms ahead - serialization!

Now, lets add in some extra stuff to help with Google data like like etags and some default versioning headers.


class Feed
{
    /**
     * Etag
     * @var string
     * @see http://code.google.com/apis/gdata/docs/2.0/reference.html#ResourceVersioning
     */
    public $etag;
    
    /**
     * A list of namespaces used in this feed
     * @var array
     */
    public $namespaces = array();
    
    /**
     * Internal xml object
     * @var SimpleXMLElement
     */
    protected $_xml;
    
    /**
     * Default cUrl headers
     * @var array
     */
    private static $default_headers = array(
        'GData-Version: 2',
    );
            
    /**
     * Constructor
     */
    protected function __construct($feed)
    {
        $this->_xml = $feed;
        
        if (is_string($this->_xml))
        {
            $this->_xml = simplexml_load_string($feed);
        }
        
        if ($this->_xml instanceof SimpleXMLElement)
        {
            $this->namespaces = (array)$this->_xml->getDocNamespaces();
            $this->etag = (string) $this->_xml->attributes($this->namespaces['gd'])->etag;
        }
    }
    
    /**
     * Get variables from the xml feed
     * @param string $var
     * @return mixed
     */
    public function __get($var)
    {
        // Allow namespaces to be accessed as $this->namespace->var...
        if (array_key_exists($var, (array)$this->namespaces))
        {
            return $this->_xml->children($this->namespaces[$var]);
        }
        else if ($var == 'namespaces')
        {
            return $this->namespaces;
        }
        else
            return $this->_xml->$var;
    }

    /**
     * Load a feed either from the web or from the cache
     * @param string $url
     * @param string $class
     * @return Feed
     */
    public static function load($url, $class = 'Feed')
    {
        if (!IN_PRODUCTION)
            cache::delete($url);    // Always clear cache in dev mode.
        
        // Check if there is a cached version.
        $feed = cache::read($url);

        // Try and get the feed using the etag.
        try
        {
            $headers = self::$default_headers;
            
            if ($feed !== null)
            {
                $headers[] = 'If-None-Match: '.$feed->etag;
            }
        
            /**
             * @see http://www.php.net/manual/en/function.curl-setopt.php
             */
            $raw_feed = remote::get(
                $url,
                array(
                    CURLOPT_HTTPHEADER => $headers,
                    // CURLOPT_HEADER => true,    // For debugging
            ));
            
            $feed = new $class($raw_feed);
            
            cache::write($url, $feed);
        }
        catch (Exception $e)
        {
            // Catch 304 errors - Content Not Modified
            if ($e->getCode() == 304)
            {
                // Return cached version
                return $feed;
            }
            else
                throw $e;
        }
        
        return $feed;
    }
    
    /**
     * __wakeup - unserialize
     */
    public function __wakeup()
    {
        /*
         * Reload the SimpleXMLElement from the raw,
         * serialized string.
         */
        $this->_xml = simplexml_load_string($this->_xmlRaw);
        unset($this->_xmlRaw);
    }
    
    /**
     * __sleep - serialize
     */
    public function __sleep()
    {
        /*
         * The SimpleXMLElement $this->_xml can't be serialized,
         * so we have to to get the raw xml feed.  However, this
         * instance may still be used after serialization, so
         * we can't just reassign $this->_xml.  We use a temp
         * variable, _xmlRaw, to store the raw feed for
         * serialization.  
         */
        if ($this->_xml instanceof SimpleXMLElement)
            $this->_xmlRaw = $this->_xml->asXML();
        
        $members = array_keys(get_object_vars($this));
        unset($members[ array_search('_xml', $members) ]);
        return $members;
    }
}

One caveat - this is meant to work with Kohana 3, but could be easily adapted to standalone code. With Kohana though, the remote::get() call needs one fix to work correctly. The vanilla code throws an exception on anything but HTTP 200 responses, okay, however the exception does not include the returned status. Google's etag will return 304 if the feed has not been modified. See this issue. The fix is really simple:

Kohana: remote.php

// Close the connection
curl_close($remote);
 
if (isset($error))
{
    throw new Kohana_Exception('Error fetching remote :url [ status :code ] :error',
        array(':url' => $url, ':code' => $code, ':error' => $error),
        $code);
}
 
return $response;

Usage

So now what?

We have an object that can be instantiated, but there's also a static method - load(), that handles caching the feed and checking the etag. We can even have it return an instance that inherits from Feed and does some extra processing to the feed.

load($url, $class = 'Feed')

Static method that loads the feed at the given url. Handles caching the feed and checking against the etag to see if the feed has been updated. This saves you response time

__get($var)

Ah magic methods. This lets us access the SimpleXMLElement as $feed->title, $feed->entry, etc... It also adds a bit to auto-children namespaces. $feed->gdata->id, etc...

$feed = feed::load($url);

echo $feed->title;
echo $feed->gdata->id;
echo $feed->link->attributes()->href;

foreach ($feed->entry as $entry)
{
    // Loop all the entries from this feed
}

Geodesic Map - Borders

Just posting an older picture showing borders working.

The borders are a separate mesh layer drawn over the cells (just turn z-indexing off). While the cell mesh never changes from build time (except changing out texture paging), the border mesh is built entirely at run-time. Given that it has a low frequency of changes, this is okay. The mesh can be built quickly whenever the borders change.

Tuesday, February 16, 2010

Simple Serializer for C#

Nothing too special here, just a utility class to help serialize objects quickly and easily.


public static class Serializer
{
    /// <summary>
    /// Serialize the given object into UTF-8 encoded XML
    /// </summary>
    /// <param name="item"></param>
    /// <returns></returns>
    public static string Serialize(object item)
    {
        if (item == null)
        {
            throw new ArgumentNullException("item");
        }

        using (var stream = new MemoryStream())
        {
            // This constructor is automatically cached, so no need for the
            // local cache.
            var serializer = new XmlSerializer(item.GetType());

            serializer.Serialize(stream, item);
            return Encoding.UTF8.GetString(stream.ToArray());
        }
    }

    /// <summary>
    /// Deserialize the given UTF-8 encoded XML string
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="xml"></param>
    /// <returns></returns>
    public static T Deserialize<T>(string xml)
    {
        using (var stream = new MemoryStream(xml.ToByteArray()))
        {
            // This constructor is automatically cached, so no need for the
            // local cache.
            var serializer = new XmlSerializer(typeof(T));

            return (T)serializer.Deserialize(stream);
        }
    }
}

Usage is pretty simple:

MyClass instance = new MyClass();
Serializer.Write<MyClass>(instance, "instance.xml");

MyClass instance_2 = Serializer.Read<MyClass>("instance.xml");

Edit, 1/30/13


When targeting heirarchies and inherited types, the XML serializer can leak memory. Basically, when you create a new serializer, it will create an assembly at runtime. The basic constructors of XmlSerializer will cache this assembly automatically on their own, but when working with an inherited type (this constructor), it won't be. So, here's an expanded helper that works with inherited types and caches XmlSerializers.


Updated Xml Helper

Wednesday, February 3, 2010

PHP Cache Class

While Kohana has an internal caching function that works quite well, I wanted a bit more robust handling. Namely, I wanted to be able to set a cache file that never expired - something Kohana 3's cache function doesn't handle. Also, I found it awkward to have to pass the lifespan of the cache into read function. I found some sample code where you can play with the touch() function to set an m time in the future. When the m and c times are equal, we have an indefinite cache file

There are only a few situations where you'd want an indefinite cache, and it leaves handling expiration up to the function doing the caching. In my current case, this is reading ATOM feeds from Picasa. The feed headers allow the feed to be queried to see if the feed has been updated.

  • Attempt to get cached feed to get the etag field.
  • Retrieve the feed using the If-None-Match: header
  • Evaluate response:
    • If response is HTTP 304 - content is not modified, use cached feed. In this case the entire feed is not sent back in the response.
    • If response is HTTP 200 - content is modified, take feed and update cache

Code

Much of this is based on Kohana's caching function with some file locking tossed in.
This is an early version - not tested much, beware!
/**
 * Cache helper
 *
 * @package    Cache
 * @author     Levi Baker
 */
class Cache
{
    /**
     * @var directory permissions for the cache directories.
     */
    private static $cache_dir_permissions = 0700;

    /**
     * Cache a resource
     * @param string $name - resource name
     * @param mixed $data - data to be serialized and cached.
     * @param int $lifetime - ttl of the data in seconds.
     */
    public static function write($name, $data, $lifetime = null)
    {
        $file = self::name($name);
        $dir = self::dir($name);
    
        try
        {
            // Ensure that the directory has been created.
            if ( ! is_dir($dir))
            {
                // Create the cache directory
                mkdir($dir, self::$cache_dir_permissions, TRUE);
    
                // Set permissions (must be manually set to fix umask issues)
                chmod($dir, self::$cache_dir_permissions);
            }
    
            // Open the file.
            $f = fopen($dir.$file, 'w');
            
            // Get an exlusive lock on the file while writing
            flock($f, LOCK_EX);
            
            // Write to cach
            if (fwrite($f, serialize($data)) === false)
                throw new Exception('Could not write to cache file.');
            
            fclose($f);    // close and release the lock.
            
            
            // Set lifetime
            if ( ! touch($dir.$file, time() + (int) $lifetime))
                throw new Exception('Could not touch cache file');
                
            return true;
        }
        catch (Exception $e)
        {
            throw $e;
        }
    }
    
    /**
     * Read from a cache resource
     * @param string $name - resource name
     * @return mixed - unserialized resource, or null if not found.
     */
    public static function read($name)
    {
        try
        {
            $path = self::dir($name).self::name($name);

            if (is_file($path))
            {
                // Check if a lifetime was set.
                if (filemtime($path) > filectime($path))
                {
                    // cache file is expired.
                    if (time() > filemtime($path))
                    {
                        unlink ($path);
                        return null;
                    }
                }
                
                // We need a file pointer to do a lock
                $f = fopen($path,'r');

                if (!$f)
                    return null;
                
                // Get a shared lock
                flock($f, LOCK_SH);
                
                $data = file_get_contents($path);
                fclose($f);
                
                return unserialize($data);
            }
            
            // Cache not found.
            return null;
        }
        catch (Exception $e)
        {
            throw $e;
        }
    }
    
    /**
     * Delete a cached resource
     * @param string $name - resource name
     */
    public static function delete($name)
    {
        $path = self::dir($name).self::name($name);

        if (is_file($path))
        {
            return unlink ($path);
        }
        return false;
    }
    
    /**
     * Name - returns the name of the cache file
     * @param string $name
     */
    private static function name($name)
    {
        return sha1($name).'.txt';
    }
    
    /**
     * Directory - returns the full path of the cache file
     * @param string $name
     */
    private static function dir($name)
    {
        $file = self::name($name);
        return Kohana::$cache_dir.DIRECTORY_SEPARATOR.$file[0].$file[1].DIRECTORY_SEPARATOR;
    }
}

While this is setup to run with Kohana 3, it could quite easily be made standalone by modifying the function dir($name) to set the cache directory elsewhere.

Using this class is pretty simple:

// Write to a cache with no expiration
cache::write('resource.name', $my_cool_data);

// Write to a cache with a lifespan of 5 minutes
cache::write('resource.expires', $temporary_data, 300);

// Read data
$data = cache::read('resource.name');

// Check that the resource didn't expire though...
if ($data !== null)
{
    // Do things!
}

Notes

This uses serialize on the data coming in, so be sure that the data being saved is serializable. Some php classes have issues with serialization. Look into the __sleep() and __wakeup() magic functions to extend and enable serialization on objects.

css @import troubles

Interesting read on why the @import rule should be avoided.  Should use <link> instead to help speed up loading and preserve load order.

High Performance Web Sites blog