< / >

This is a blog by about coding and web development.

Loading embedded assets at runtime

Posted on in

The Embed ActionScript metadata tag is a common way to include external files in a SWF. Quick review! Let’s say you had these files:

.
├── assets
│   ├── test.mp3
│   ├── test.png
│   └── test.xml
├── Test.as
├── test.html
└── test.swf

You could embed and use these assets like so:

package {
  import flash.display.Bitmap;
  import flash.display.Sprite;
  import flash.media.Sound;
  import flash.utils.ByteArray;

  public class Test extends Sprite {
    [Embed(source='assets/test.png')]
    static public var TEST_BITMAP:Class;

    [Embed(source='assets/test.mp3')]
    static public var TEST_SOUND:Class;

    [Embed(source='assets/test.xml', mimeType='application/octet-stream')]
    static public var TEST_XML:Class;

    function Test() {
      // Types Flash knows how to encode becomes normal Flash objects.
      var bitmap:Bitmap = new TEST_BITMAP;
      addChild(bitmap);
      var sound:Sound = new TEST_SOUND;
      sound.play();

      // Other types get the octet-stream mimeType, and end up as a
      // raw ByteArray, which you can read yourself.
      var bytes:ByteArray = new TEST_XML;
      var text:String = bytes.readUTFBytes(bytes.length);
      var xml:XML = new XML(text);
      trace(xml);
    }
  }
}

All of the content in our current game is embedded this way. But, as the game gets closer to release, this setup has been getting frustrating. We need to recompile every time we want to test a change to a level, or image, or sound.

To fix this, I made the debug SWF load things on the fly, and use the embedded assets in release mode. We can save a level file, and restart the level to see the changes immediately – without recompiling or reloading the page.

This required doing several things: * Using reflection to find the embedded file’s path * Downloading the files instead of instantiating the classes * Stripping the debug download code in release mode

Finding the embedded file’s path

Given a Class variable, you can use Flash’s describeType() to get XML information about the variable, including attached tags. For example, running trace(describeType(TEST_BITMAP)) on the above variable would return:

<type name="Test_TEST_BITMAP" base="Class" isDynamic="true"
  isFinal="true" isStatic="true">
  <extendsClass type="Class"/>
  <extendsClass type="Object"/>
  <accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
  <factory type="Test_TEST_BITMAP">
    <extendsClass type="mx.core::BitmapAsset"/>
    <extendsClass type="mx.core::FlexBitmap"/>
    <extendsClass type="flash.display::Bitmap"/>
    <extendsClass type="flash.display::DisplayObject"/>
    <extendsClass type="flash.events::EventDispatcher"/>
    <extendsClass type="Object"/>
    ... snip ...
    <metadata name="Embed">
      <arg key="_resolvedSource" value="/path/to/assets/test.png"/>
      <arg key="_column" value="5"/>
      <arg key="source" value="assets/test.png"/>
      <arg key="exportSymbol" value="Test_TEST_BITMAP"/>
      <arg key="_line" value="9"/>
      <arg key="_file" value="/path/to/Test.as"/>
    </metadata>
    <metadata name="ExcludeClass"/>
    <metadata name="__go_to_ctor_definition_help">
      <arg key="file" value="Test_TEST_BITMAP.as"/>
      <arg key="pos" value="323"/>
    </metadata>
    <metadata name="__go_to_definition_help">
      <arg key="file" value="Test_TEST_BITMAP.as"/>
      <arg key="pos" value="253"/>
    </metadata>
  </factory>
</type>

I’ve snipped most of it (the full output is huge), but you can see the relevant metadata element at the bottom. We can use this to figure out the path to load. You can use Flash’s E4X parsing to extract the attribute:

var xml:XML = describeType(TEST_BITMAP);
var embedMetadata:XML = xml.factory.metadata.(@name == 'Embed');
var sourceArg:XML = embedMetadata.arg.(@key == 'source');
var path:String = sourceArg.@value;

// or, shorter:
path = (describeType(TEST_BITMAP).factory.metadata.(@name == 'Embed')
  .arg.(@key == 'source').@value);

Downloading the file

Once you have the paths, you’ll need to use classes like Loader to download the files on the fly. This means you’ll need to serve the assets over HTTP, unless you’re using AIR. I used a Ruby Sinatra server for this:

require 'rubygems'
require 'sinatra'

set :public, File.dirname(__FILE__)

I saved this as server.rb, next to Test.as. You run it with ruby server.rb (or by double clicking it, on Windows), and you’ll see something like:

$ ruby server.rb
== Sinatra/1.1.2 has taken the stage on 4567 for development with backup from Thin
>> Thin web server (v1.2.8 codename Black Keys)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:4567, CTRL+C to stop

If you don’t have Ruby, you can download it here. If you don’t have Sinatra, you can install it by running gem install sinatra.

Once it is running, you can test it in a browser. In my example, the URL for test.png would be http://localhost:4567/assets/test.png. Now the ActionScript code needs to load these URLs. I added some helper methods:

static public function getURLRequest(cls:Class):URLRequest {
  var path:String = (describeType(cls).factory.metadata.(@name == 'Embed')
    .arg.(@key == 'source').@value);
  return new URLRequest(path);
}

static public function getBitmap(cls:Class, callback:Function):void {
  var loader:Loader = new Loader;
  loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(event:Event):void {
    callback(loader.content);
  });
  loader.load(getURLRequest(cls));
}

static public function getBytes(cls:Class, callback:Function):void {
  var loader:URLLoader = new URLLoader;
  loader.dataFormat = URLLoaderDataFormat.BINARY;
  loader.addEventListener(Event.COMPLETE, function(event:Event):void {
    callback(loader.data);
  });
  loader.load(getURLRequest(cls));
}

static public function getSound(cls:Class, callback:Function):void {
  var sound:Sound = new Sound;
  sound.addEventListener(Event.COMPLETE, function(event:Event):void {
    callback(sound);
  });
  sound.load(getURLRequest(cls));
}

If this were real production code, you would want to catch error events. However, since this is just debug test code, I’m only listening for the COMPLETE event.

Now, instead of using new TEST_BITMAP, you call these methods:

getBitmap(TEST_BITMAP, function(bitmap:Bitmap):void {
  addChild(bitmap);
});

getSound(TEST_SOUND, function(sound:Sound):void {
  sound.play();
});

getBytes(TEST_XML, function(bytes:ByteArray):void {
  var text:String = bytes.readUTFBytes(bytes.length);
  var xml:XML = new XML(text);
  trace(xml);
});

Fixing caching issues

I quickly ran into a problem: Flash was only downloading the file once. This was because it was caching the assets. To stop Flash from caching, the HTTP server needs to send Cache-Control and Pragma headers. For a Sinatra server, you can use a Rack middleware:

require 'rubygems'
require 'sinatra'

class DisableCache
  def initialize(app)
    @app = app
  end

  def call(env)
    result = @app.call(env)
    result[1]['Cache-Control'] = 'no-cache, no-store, must-revalidate'
    result[1]['Pragma'] = 'no-cache'
    return result
  end
end

use DisableCache

set :public, File.dirname(__FILE__)

If you clear your cache and try again, you should see the SWF downloading the assets every time.

Disabling in release mode

I only wanted our SWF to download assets like this in debug mode. I could have done this with a debug Boolean:

static public function getSound(cls:Class, callback:Function):void {
  if (debug) {
    var sound:Sound = new Sound;
    sound.addEventListener(Event.COMPLETE, function(event:Event):void {
      callback(sound);
    });
    sound.load(getURLRequest(cls));
  } else {
    callback(new cls);
  }
}

However, in my case, I didn’t even want the loader code in my SWF in release mode. I did this with the -define command line option for mxmlc. This option allows you to do conditional compilation with constants. When I compile in debug mode, my command line looks like this:

mxmlc -debug -define+=ENV::debug,true -define+=ENV::release,false \
  -static-rsls -output=test.swf Test.as

And in release, it looks like this:

mxmlc -define+=ENV::debug,false -define+=ENV::release,true \
  -static-rsls -output=test.swf Test.as

Then I updated the helper functions to use these constants:

static public function getSound(cls:Class, callback:Function):void {
  ENV::debug {
    var sound:Sound = new Sound;
    sound.addEventListener(Event.COMPLETE, function(event:Event):void {
      callback(sound);
    });
    sound.load(getURLRequest(cls));
  }
  ENV::release {
    callback(new cls);
  }
}

The syntax is a bit strange for these conditionals. It’s basically the AS3 version of a C preprocessor #ifdef. If the constant is false, the attached block is completely stripped from the compiled SWF.

When all is said and done, you have a SWF that uses embedded assets with only the overhead of a function call, and assets that are loaded on the fly for debug mode. You could take this even further by doing things like caching, or monitoring files and automatically refreshing them while the SWF is running… but I will leave that for a future post.

blog comments powered by Disqus