Last active
July 13, 2017 11:56
-
-
Save Maligan/994691ec8ecd27d7a991309f044b9c64 to your computer and use it in GitHub Desktop.
AssetManager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package | |
{ | |
import flash.display.Bitmap; | |
import flash.display.Loader; | |
import flash.events.ErrorEvent; | |
import flash.events.Event; | |
import flash.events.HTTPStatusEvent; | |
import flash.events.IOErrorEvent; | |
import flash.events.SecurityErrorEvent; | |
import flash.net.URLLoader; | |
import flash.net.URLLoaderDataFormat; | |
import flash.net.URLRequest; | |
import flash.system.ImageDecodingPolicy; | |
import flash.system.LoaderContext; | |
import flash.utils.ByteArray; | |
import flash.utils.Dictionary; | |
import starling.text.BitmapFont; | |
import starling.text.TextField; | |
import starling.textures.AtfData; | |
import starling.textures.Texture; | |
import starling.textures.TextureAtlas; | |
import starling.textures.TextureOptions; | |
/** | |
* Usage: | |
* | |
* var assets:AssetManager = new AssetManager(); | |
* assets.handle("path/to/file1"); | |
* ... | |
* assets.handle("path/to/fileN"); | |
* | |
* And show trace log :-) | |
* | |
* This sample support next type of assets: | |
* - Texture (png, jpg, gif, atf) | |
* - JSON | |
* - XML | |
* - BitmapFont | |
* - TextureAtlas | |
*/ | |
public class AssetManager | |
{ | |
// http://help.adobe.com/en_US/as3/dev/WS5b3ccc516d4fbf351e63e3d118a9b90204-7d66.html | |
// http://en.wikipedia.org/wiki/List_of_file_signatures | |
public static const IMAGE_FORMAT_SIGNATURES:Array = | |
[ | |
"\u0089PNG\r\n\u001A\n", // PNG | |
"\u00FF\u00D8\u00FF", // JPG (not full there are 3 forms) | |
"\u0047\u0049\u0046\u0038\u0037\u0061", // GIF87a | |
"\u0047\u0049\u0046\u0038\u0039\u0061" // GIF89a | |
]; | |
public static const DOWNLOAD_TRIES:int = 3; | |
protected var _handlers:Vector.<Handler>; | |
protected var _registry:Dictionary; | |
protected var _shared:Object; | |
private var _verbose:Boolean = true; | |
private var _checkPolicyFile:Boolean = false; | |
public function AssetManager() | |
{ | |
_registry = new Dictionary(); | |
_handlers = new Vector.<Handler>(); | |
_shared = new Object(); | |
addHandler(loadURL, URLRequest); | |
addHandler(decodeStringToURLRequest, String); | |
addHandler(decodeByteArrayToTexture, ByteArray); | |
addHandler(decodeByteArrayToXML, ByteArray); | |
addHandler(decodeByteArrayToJSON, ByteArray); | |
addHandler(assembleTextureAtlas, Object); | |
addHandler(assembleBitmapFont, Object); | |
} | |
// | |
// Basic API | |
// | |
public function addHandler(callback:Function, type:Class = null, priority:int = 0):void | |
{ | |
// Param 'type' define which type of asset handler can process | |
// this info used as rough filter in handle() method. | |
// Type added only for remove excess types check within each handler. | |
_handlers.push(new Handler(callback, type || Object, priority)); | |
_handlers.sort(byPriority); | |
} | |
private function byPriority(h1:Handler, h2:Handler):int | |
{ | |
return h1.priority - h2.priority | |
} | |
public function handle(asset:Object, key:String = null, options:Object = null):int | |
{ | |
for each (var handler:Handler in _handlers) | |
{ | |
if (asset is handler.type) | |
{ | |
var status:int = null; | |
if (handler.callback.length == 1) status = handler.callback(asset); | |
else if (handler.callback.length == 2) status = handler.callback(asset, key); | |
else status = handler.callback(asset, key, options); | |
// Prevent next processing (each handler can stop next processing via return status = -1) | |
if (status === -1) break; | |
} | |
} | |
return 0; | |
} | |
/** This is generic method can be used with wrappers: getTexture, getObject, getSound, getBytes etc. */ | |
public final function getAsset(type:Class, key:String):Object | |
{ | |
return _registry[type] ? _registry[type][key] : null; | |
} | |
public final function addAsset(type:Class, key:String, asset:Object):void | |
{ | |
_registry[type] ||= new Dictionary(); | |
_registry[type][key] = asset; | |
log("Added " + type.toString().substr(7).substr(0, -1) + "::" + key); | |
} | |
public function removeAsset(type:Class, key:String):void | |
{ | |
if (type in _registry) delete _registry[type][key] | |
} | |
// | |
// Conveyor Handerls | |
// | |
protected function loadURL(value:URLRequest, key:String, options:Object):void | |
{ | |
download(value, complete, DOWNLOAD_TRIES); | |
function complete(result:Object):void | |
{ | |
if (result is ByteArray) | |
handle(result, key || getName(value.url), options); | |
else if (result is Error) | |
log(Error(result).message); | |
} | |
} | |
protected function decodeStringToURLRequest(value:String, key:String, options:Object):void | |
{ | |
handle(new URLRequest(value), key, options); | |
} | |
protected function decodeByteArrayToTexture(value:ByteArray, key:String, options:Object):void | |
{ | |
if (isBitmapData(value)) | |
{ | |
var loaderContext:LoaderContext = new LoaderContext(_checkPolicyFile); | |
loaderContext.imageDecodingPolicy = ImageDecodingPolicy.ON_LOAD; | |
var loader:Loader = new Loader(); | |
loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, complete); | |
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, complete); | |
loader.loadBytes(value, loaderContext); | |
function complete(e:Event):void | |
{ | |
loader.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, complete); | |
loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, complete); | |
if (e.type == Event.COMPLETE) | |
{ | |
var bitmap:Bitmap = Bitmap(loader.content); | |
var texture:Texture = Texture.fromData(bitmap, options as TextureOptions); | |
addAsset(Texture, key, texture); | |
handle(texture, key, options); | |
} | |
} | |
} | |
else if (AtfData.isAtfData(value)) | |
{ | |
var texture:Texture = Texture.fromData(value, options as TextureOptions); | |
addAsset(Texture, key, texture); | |
handle(texture, key); | |
} | |
} | |
protected function decodeByteArrayToXML(value:ByteArray, key:String):void | |
{ | |
if (checkFirstMeanigfulLetter(value, "<")) | |
{ | |
var xml:XML = null; | |
try { xml = new XML(value); } | |
catch (e:Error) { /* NOP */ } | |
if (xml) | |
{ | |
addAsset(XML, key, xml); | |
handle(xml, key); | |
} | |
} | |
} | |
protected function decodeByteArrayToJSON(value:ByteArray, key:String):void | |
{ | |
if (checkFirstMeanigfulLetter(value, "{") || checkFirstMeanigfulLetter(value, "[")) | |
{ | |
var object:Object = null; | |
try { object = JSON.parse(value.toString()); } | |
catch (e:Error) { /* NOP */ } | |
if (object) | |
{ | |
addAsset(Object, key, object); | |
handle(object, key); | |
} | |
} | |
} | |
protected function assembleTextureAtlas(value:Object, key:String):void | |
{ | |
var waiting:Object = (_shared["assembleTextureAtlas"] ||= {}); | |
if (value is XML && XML(value).localName() == "TextureAtlas") | |
{ | |
var textureKey:String = getName(XML(value).@imagePath); | |
var texture:Texture = Texture(getAsset(Texture, textureKey)); | |
if (texture == null) waiting[textureKey] = [key, value]; // TODO: it is possible multiple links to one texture | |
else importAtlas(key, texture, XML(value)); | |
} | |
else if (value is Texture) | |
{ | |
if (waiting[key] != null) | |
{ | |
var atlasKey:String = waiting[key][0]; | |
var atlasXML:XML = waiting[key][1]; | |
importAtlas(atlasKey, Texture(value), atlasXML); | |
delete waiting[key]; | |
} | |
} | |
} | |
protected function assembleBitmapFont(value:Object, key:String):void | |
{ | |
var waiting:Object = (_shared["assembleBitmapFont"] ||= {}); | |
if (value is XML && XML(value).localName() == "font") | |
{ | |
var textureKey:String = getName(XML(value).pages.page.@file); | |
var texture:Texture = Texture(getAsset(Texture, textureKey)); | |
if (texture == null) waiting[textureKey] = [key, value]; | |
else registerBitmapFont(texture, XML(value)); | |
} | |
else if (value is Texture) | |
{ | |
if (waiting[key] != null) | |
{ | |
var atlasXML:XML = waiting[key][1]; | |
registerBitmapFont(Texture(value), atlasXML); | |
delete waiting[key]; | |
} | |
} | |
} | |
// | |
// Utility methods | |
// | |
private function download(url:URLRequest, callback:Function, numTries:int):void | |
{ | |
var urlLoader:URLLoader = new URLLoader(); | |
var urlStatus:int = 0; | |
urlLoader.addEventListener(Event.COMPLETE, urlLoaderComplete); | |
urlLoader.addEventListener(IOErrorEvent.IO_ERROR, urlLoaderComplete); | |
urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, urlLoaderComplete); | |
urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, urlLoaderStatusEvent); | |
urlLoader.dataFormat = URLLoaderDataFormat.BINARY; | |
urlLoader.load(url); | |
function urlLoaderStatusEvent(e:HTTPStatusEvent):void | |
{ | |
urlStatus = e.status; | |
} | |
function urlLoaderComplete(e:Event):void | |
{ | |
urlLoader.removeEventListener(Event.COMPLETE, urlLoaderComplete); | |
urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, urlLoaderComplete); | |
urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, urlLoaderComplete); | |
urlLoader.removeEventListener(HTTPStatusEvent.HTTP_STATUS, urlLoaderStatusEvent); | |
if (e.type == Event.COMPLETE) | |
callback(urlLoader.data); | |
else if (e.type == IOErrorEvent.IO_ERROR && numTries>0 && (urlStatus==0 || urlStatus==408 || urlStatus==503)) | |
download(url, callback, numTries-1); | |
else | |
callback(new Error(ErrorEvent(e).text + ". RC: " + urlStatus)); | |
} | |
} | |
protected function isBitmapData(bytes:ByteArray):Boolean | |
{ | |
for each (var signature:String in IMAGE_FORMAT_SIGNATURES) | |
if (checkSignature(bytes, signature)) | |
return true; | |
return false; | |
} | |
private function importAtlas(key:String, texture:Texture, xml:XML):void | |
{ | |
var atlas:TextureAtlas = new TextureAtlas(texture, xml); | |
addAsset(TextureAtlas, key, atlas); | |
for each (var textureName:String in atlas.getNames()) | |
addAsset(Texture, textureName, atlas.getTexture(textureName)); | |
} | |
private function registerBitmapFont(texture:Texture, xml:XML):void | |
{ | |
var font:BitmapFont = new BitmapFont(texture, xml); | |
TextField.registerCompositor(font, font.name); | |
log("Added BitmapFont::" + font.name); | |
} | |
/** Extracts the base name of a file path or URL, i.e. the file name without extension. */ | |
protected final function getName(url:String):String | |
{ | |
var NAME_REGEX:RegExp = /([^\?\/\\]+?)(?:\.([\w\-]+))?(?:\?.*)?$/; | |
var matches:Array = NAME_REGEX.exec(url); | |
if (matches && matches.length > 0) return matches[1]; | |
else return null; | |
} | |
/** Check whenever byte array starts with signature. */ | |
protected final function checkSignature(bytes:ByteArray, signature:String):Boolean | |
{ | |
if (bytes.bytesAvailable < signature.length) return false; | |
for (var i:int = 0; i < signature.length; i++) | |
if (signature.charCodeAt(i) != bytes[i]) return false; | |
return true; | |
} | |
protected final function checkFirstMeanigfulLetter(bytes:ByteArray, char:String):Boolean | |
{ | |
var start:int = 0; | |
var length:int = bytes.length; | |
var wanted:int = char.charCodeAt(0); | |
// recognize BOMs | |
if (length >= 4 && | |
(bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xfe && bytes[3] == 0xff) || | |
(bytes[0] == 0xff && bytes[1] == 0xfe && bytes[2] == 0x00 && bytes[3] == 0x00)) | |
{ | |
start = 4; // UTF-32 | |
} | |
else if (length >= 3 && bytes[0] == 0xef && bytes[1] == 0xbb && bytes[2] == 0xbf) | |
{ | |
start = 3; // UTF-8 | |
} | |
else if (length >= 2 && | |
(bytes[0] == 0xfe && bytes[1] == 0xff) || (bytes[0] == 0xff && bytes[1] == 0xfe)) | |
{ | |
start = 2; // UTF-16 | |
} | |
// find first meaningful letter | |
for (var i:int=start; i<length; ++i) | |
{ | |
var byte:int = bytes[i]; | |
if (byte == 0 || byte == 10 || byte == 13 || byte == 32) continue; // null, \n, \r, space | |
else return byte == wanted; | |
} | |
return false; | |
} | |
public function log(message:String):void | |
{ | |
if (_verbose) trace("[AssetManager]", message); | |
} | |
} | |
} | |
class Handler | |
{ | |
public var callback:Function; | |
public var type:Class; | |
public var priority:int; | |
public function Handler(callback:Function, type:Class, priority:int) | |
{ | |
this.callback = callback; | |
this.type = type; | |
this.priority = priority; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment