运行时加载嵌入的资源

原文地址在此

ActionScript的元数据标签Embed是一种在SWF里面包含外部文件常用的方法.快速复习一下,比如说你有如下结构的文件:

    .
    ├── assets
    │   ├── test.mp3
    │   ├── test.png
    │   └── test.xml
    ├── Test.as
    ├── test.html
    └── test.swf
你能将资源嵌入并像如下使用:

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);
		}
	}
}
我们现在的游戏全部内容都是使用这种方式嵌入到游戏中的.但是在游戏快要发布时,这种设置就就变得很叫人不爽.每次我们需要测试对(关卡)等级或者图像或者声音所做的改进效果时,我们就得重新编译.

要解决这问题,我设置调试版本的SWF动态加载资源,并且在发布模式(release mode)中使用嵌入资源(embedded assets).我们可以保存一个关卡文件(level file)并重启关卡来立即看到游戏的变化且无需重新编译或者重新加载页面(reloading the page)

这需要我们做以下事情:

  • 使用反射找到嵌入的资源路径
  • 加载文件而不是实例化类
  • 在发布模式中剥离调试模式里的下载代码(debug download code)

找到嵌入文件的路径

给定一个类变量,你可以用Flash的describeType()来得到关于变量的XML信息,包括附加的标签.例如,对上面代码执行trace(describeType(TEST_BITMAP))将会返回:

    <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>
我已经删减了大部分(全部 输出的信息很多),但你可在底部看到相关的metadata元素.我们可以用这个来断定加载的路径.你可以使用Flash的 E4X解析器取出如下属性:

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);

加载文件

一旦有了路径,你需要使用像Loader这样的类来动态的加载文件.这意味着除非你使用AIR,你需要通过HTTP提供资源(server the assets over HTTP).对此我使用了一个Ruby的Sinatra服务器

 require 'rubygems'
    require 'sinatra'
    
    set :public, File.dirname(__FILE__)
我将其保存为server.rb,紧挨着Test.as.使用命令ruby server.rb(或者在windows上双击该文件)你就会看到类似如下的东西:

  $ 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
如果你没有安装Ruby,你可以在 这里下载.如果你没有Sinatra你可以运行命令gem install sinatra来安装它.

一旦开始运行后,你可以在浏览器中测试它.在我的例子中,test.png的URL是http://localhost:4567/assets/test.png.现在ActionScript代码需要家在这些URL了.我添加了一些辅助方法

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));
}
如果这是真实产品的代码,你可能想捕获错误事件.然而,因为这只是调试测试代码,我就只监听COMPLETE事件.

现在与其调用new TEST_BITMAP,你可以调用这些方法:

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);
});

解决缓存问题

很快我就遇到了一个问题:Flash仅仅加载一次文件.这是因为它正缓存资源文件.要阻止Flash进行缓存,HTTP服务器需要发送cache-control以及Pragma headers.对于Sinatra服务器,你可以使用一个Rack中间件.

    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__)
如果你清除缓存并再次尝试,你就会发现SWF每次都加载资源文件.

在发布模式中禁用它

我们只想要我们的SWF在调试模式中像这样加载资源.我可以通过一个debug布尔变量搞定它.

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);
	}
}
然而,对我而言我甚至不愿意加载的代码在我发布版的SWF中.我使用mxmlc的-define 命令行选项.这个选项允许你使用常量做做条件编译.当我在debug模式下编译时,我的命令行如下:

mxmlc -debug -define+=ENV::debug,true -define+=ENV::release,false -static-rsls -output=test.swf Test.as
在发布模式下:如下所示:

mxmlc -define+=ENV::debug,false -define+=ENV::release,true -static-rsls -output=test.swf Test.as
之后我更新了辅助函数来使用这些常量.

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);
	}
}
这里条件语句语法可能有点奇葩.它基本上是AS3版的C预处理器#ifdef.如果常量为false,其后附加的代码块将被完全的从编译的SWF中剥离.

结果是你有了一个使用嵌入资源的SWF文件,它只使用一个函数调用就使得在debug模式下资源文件可以动态加载.你可以借此一步做做这些事情,比如缓存或者在SWF运行时监视文件并自动刷新它们...但这些就留在以后的文章中说吧.

你可能感兴趣的:(Flash,动态加载,游戏开发,条件编译,调试模式)