15 KiB
OpenFL Build Chain: Asset System Deep Dive
This document provides an in-depth analysis of OpenFL's asset handling system, focusing on asset processing, the runtime access layer, and resource management. This information is intended to help implement a custom asset bundling system that bypasses the standard OpenFL/Lime build toolchain.
1. Asset Processing System
OpenFL's asset processing pipeline transforms source files into platform-optimized assets through several stages:
1.1 Asset Identification
Implementation:
- The asset processing begins with scanning directories specified in
project.xml - For each asset, OpenFL records:
- Source path: Location in the project directory
- Target path: Location in the output bundle
- ID: Unique identifier (typically the target path)
- Type: Determined by file extension or explicit definition
// Simplified representation of how OpenFL identifies assets
var assets = [];
for (assetPath in FileSystem.readDirectory(sourcePath)) {
if (filterPattern.match(assetPath)) {
var id = targetPath + assetPath.substr(sourcePath.length);
var type = determineAssetType(assetPath);
assets.push({ id: id, path: assetPath, type: type });
}
}
1.2 Type-Specific Processing
OpenFL processes different asset types with specialized handlers:
Images (BitmapData)
- Formats: PNG, JPG, GIF, BMP, etc.
- Processing:
- Conversion to platform-specific formats
- Optional compression
- Resolution handling for multi-DPI targets
- For embedded assets, images are encoded as Base64 or binary data within generated code
// Example of image processing logic
function processImage(path, targetPath, options) {
var image = Image.fromFile(path);
// Apply transformations based on options
if (options.compress) image.compress(options.quality);
if (options.resize) image.resize(options.width, options.height);
// Output in appropriate format
if (options.embed) {
return encodeImageForEmbedding(image, options.format);
} else {
image.save(targetPath, options.format);
return { path: targetPath };
}
}
Audio
- Formats: MP3, OGG, WAV
- Processing:
- Format conversion based on platform support
- Optional compression
- Stream vs. complete load settings
Fonts
- Processing:
- Registration with the text engine
- Subsetting when specified
- Conversion to platform-specific formats
Text/Data Files
- Processing:
- Optional minification for JSON/XML
- Encoding conversion if necessary
- Direct embedding or file copying
1.3 Asset Transformation Chain
For complex asset types, OpenFL applies a chain of transformations:
- Loading: Reading the source asset
- Validation: Checking format compatibility
- Transformation: Applying format-specific conversions
- Optimization: Platform-specific optimizations
- Output: Writing to target location or embedding
1.4 Embedding Process
For embedded assets, OpenFL:
- Reads the asset content
- Converts it to a code representation (usually a byte array)
- Generates code to reconstruct the asset at runtime
- Includes this code in the compiled application
// Example of how embedding is implemented
var content = File.getBytes(assetPath);
var assetCode = 'var ${safeName}_bytes = new haxe.io.Bytes(${content.length}, "${Base64.encode(content)}");';
assetCode += 'assets.byteData.set("${assetId}", ${safeName}_bytes);';
1.5 Manifest Generation
The final step creates a manifest that catalogs all assets:
{
"name": "default",
"assets": [
{
"id": "assets/images/logo.png",
"path": "./assets/images/logo.png",
"type": "image"
},
{
"id": "assets/data/levels.json",
"path": "./assets/data/levels.json",
"type": "text"
}
]
}
2. Runtime Access Layer
The runtime access layer is responsible for loading assets during application execution.
2.1 Assets Class Architecture
OpenFL's asset system is built around several key classes:
- Assets (
openfl.utils.Assets): Static interface for accessing assets - AssetLibrary (
openfl.utils.AssetLibrary): Container for a collection of assets - AssetCache (
openfl.utils.AssetCache): Temporary storage for loaded assets
// Core Assets class simplified structure
class Assets {
private static var libraries:Map<String, AssetLibrary>;
private static var cache:AssetCache;
public static function getBitmapData(id:String, useCache:Bool = true):BitmapData;
public static function getBytes(id:String):Bytes;
public static function getFont(id:String):Font;
public static function getSound(id:String):Sound;
public static function getText(id:String):String;
}
2.2 Asset Resolution System
When requesting an asset:
- The Assets class parses the ID to determine the library and asset path
- It checks if the asset is already cached (if caching is enabled)
- If not cached, it delegates loading to the appropriate AssetLibrary
- The loaded asset is optionally cached before being returned
// Asset resolution pseudocode
function getAsset(id:String, type:AssetType) {
var libraryName = parseLibraryName(id);
var assetPath = parseAssetPath(id);
var library = libraries.get(libraryName);
if (useCache && cache.has(id)) {
return cache.get(id);
}
var asset = library.loadAsset(assetPath, type);
if (useCache) {
cache.set(id, asset);
}
return asset;
}
2.3 Library Loading Process
Asset libraries are typically loaded at application startup:
// How OpenFL initializes the asset system
public static function initializeAssets():Void {
// Load embedded manifest(s)
var manifest = AssetManifest.fromBytes(getEmbeddedManifest());
var library = AssetLibrary.fromManifest(manifest);
// Register the library
registerLibrary("default", library);
// Additional libraries can be loaded dynamically
}
2.4 Asset Loading Methods
OpenFL supports both synchronous and asynchronous loading:
- Synchronous:
Assets.getBitmapData(id)- Blocks until the asset is loaded - Asynchronous:
Assets.loadBitmapData(id).onComplete(callback)- Non-blocking load
// Example of async loading implementation
public static function loadBitmapData(id:String):Future<BitmapData> {
var libraryName = parseLibraryName(id);
var assetPath = parseAssetPath(id);
var library = libraries.get(libraryName);
return library.loadBitmapData(assetPath);
}
3. Resource Management
Resource management focuses on efficiently handling assets during runtime.
3.1 Asset Caching System
OpenFL's caching system:
- Stores loaded assets by ID in type-specific caches
- Enables efficient reuse of frequently accessed assets
- Provides cache control methods to manage memory usage
// AssetCache implementation outline
class AssetCache {
private var bitmapData:Map<String, BitmapData>;
private var font:Map<String, Font>;
private var sound:Map<String, Sound>;
public function set(id:String, asset:Dynamic, type:AssetType):Void {
switch (type) {
case AssetType.IMAGE:
bitmapData.set(id, asset);
case AssetType.FONT:
font.set(id, asset);
// Other types...
}
}
public function get(id:String, type:AssetType):Dynamic {
switch (type) {
case AssetType.IMAGE:
return bitmapData.get(id);
// Other types...
}
return null;
}
public function clear(type:AssetType = null):Void {
// Clear specific or all caches
}
}
3.2 Memory Management
OpenFL implements several strategies for efficient memory usage:
Reference Counting
- Certain assets implement reference counting to track usage
- When references drop to zero, assets can be unloaded
class ManagedAsset {
private var referenceCount:Int = 0;
public function retain():Void {
referenceCount++;
}
public function release():Void {
referenceCount--;
if (referenceCount <= 0) {
dispose();
}
}
private function dispose():Void {
// Resource-specific cleanup
}
}
Cache Eviction Policies
- Time-based: Assets unused for a period are removed
- Size-based: Cache enforces a maximum memory footprint
- Priority-based: Critical assets remain cached longer
Manual Control
Assets.cache.clear()- Clears all cached assetsAssets.cache.removeBitmapData(id)- Removes specific cached assetsAssets.unloadLibrary(name)- Unloads entire libraries
3.3 Event System
The asset system uses events to communicate state changes:
// Event system for asset loading
class AssetLibrary {
private var eventDispatcher:EventDispatcher;
public function loadBitmapData(id:String):Future<BitmapData> {
var future = new Future<BitmapData>();
// Start loading process
var loader = new BitmapDataLoader(id);
loader.onComplete = function(bitmapData) {
eventDispatcher.dispatchEvent(new AssetEvent(AssetEvent.ASSET_LOADED, id));
future.complete(bitmapData);
};
loader.onError = function(error) {
eventDispatcher.dispatchEvent(new AssetEvent(AssetEvent.ASSET_ERROR, id));
future.error(error);
};
loader.load();
return future;
}
}
4. Implementation Guidelines for Custom Asset System
Based on OpenFL's approach, here are guidelines for implementing your own asset bundling system:
4.1 Asset Processing Implementation
- Asset Scanner:
- Create a recursive directory scanner that identifies assets
- Implement pattern matching for inclusion/exclusion rules
- Generate unique IDs based on file paths
function scanAssets(directory:String, patterns:Array<String>):Array<AssetInfo> {
var assets = [];
for (file in FileSystem.readRecursive(directory)) {
if (matchesAnyPattern(file, patterns)) {
assets.push({
id: generateAssetId(file, directory),
path: file,
type: determineType(file)
});
}
}
return assets;
}
- Asset Processors:
- Create processor classes for each asset type
- Implement transformation pipelines
- Support embedding and non-embedding modes
interface AssetProcessor {
function process(source:String, target:String, options:Map<String, Dynamic>):ProcessedAsset;
function embed(source:String, options:Map<String, Dynamic>):EmbeddedCode;
}
class ImageProcessor implements AssetProcessor {
public function process(source, target, options) {
// Image-specific processing
}
public function embed(source, options) {
// Generate code for embedded image
}
}
- Manifest Generator:
- Create a serializable manifest structure
- Support JSON and binary formats
- Include asset metadata for runtime access
function generateManifest(assets:Array<AssetInfo>):AssetManifest {
var manifest = new AssetManifest();
for (asset in assets) {
manifest.addAsset(
asset.id,
asset.path,
asset.type,
asset.parameters
);
}
return manifest;
}
4.2 Runtime Access Layer Implementation
- Assets Class:
- Create a static API similar to OpenFL's Assets class
- Implement type-specific getters (getBitmapData, getText, etc.)
- Support both synchronous and asynchronous loading
class Assets {
private static var libraries:Map<String, AssetLibrary> = new Map();
private static var cache:AssetCache = new AssetCache();
public static function registerLibrary(name:String, library:AssetLibrary):Void {
libraries.set(name, library);
}
public static function getBitmapData(id:String, useCache:Bool = true):BitmapData {
// Implementation
}
public static function loadBitmapData(id:String):Future<BitmapData> {
// Async implementation
}
// Other asset type methods
}
- Asset Library:
- Implement a library class to manage asset collections
- Support manifest loading
- Handle asset type detection and loading
class AssetLibrary {
private var assets:Map<String, AssetInfo> = new Map();
public static function fromManifest(manifest:AssetManifest):AssetLibrary {
var library = new AssetLibrary();
for (asset in manifest.assets) {
library.addAsset(asset.id, asset);
}
return library;
}
public function getBitmapData(id:String):BitmapData {
var asset = assets.get(id);
if (asset != null && asset.type == AssetType.IMAGE) {
return loadBitmapDataSync(asset.path);
}
return null;
}
// Other loading methods
}
4.3 Resource Management Implementation
- Asset Cache:
- Implement a multi-type caching system
- Support cache control operations
- Add memory usage tracking
class AssetCache {
private var bitmapData:Map<String, BitmapData> = new Map();
private var text:Map<String, String> = new Map();
// Other asset type caches
public function has(id:String, type:AssetType):Bool {
switch (type) {
case AssetType.IMAGE:
return bitmapData.exists(id);
// Other types
}
return false;
}
public function set(id:String, asset:Dynamic, type:AssetType):Void {
// Implementation
}
public function get(id:String, type:AssetType):Dynamic {
// Implementation
}
public function clear(type:AssetType = null):Void {
// Implementation
}
}
- Memory Management:
- Implement reference counting for disposable assets
- Add cache size limits
- Create cache eviction policies
class MemoryMonitor {
private static var memoryUsage:Map<String, Int> = new Map();
private static var maxMemory:Int = 100 * 1024 * 1024; // 100MB example limit
public static function trackAsset(id:String, size:Int):Void {
memoryUsage.set(id, size);
enforceMemoryLimits();
}
public static function untrackAsset(id:String):Void {
memoryUsage.remove(id);
}
private static function enforceMemoryLimits():Void {
var totalUsage = calculateTotalUsage();
if (totalUsage > maxMemory) {
evictCache(totalUsage - maxMemory);
}
}
private static function evictCache(bytesToFree:Int):Void {
// Implement LRU or other eviction strategy
}
}
5. Critical Implementation Considerations
When implementing your custom asset system, keep these factors in mind:
Performance Optimization
- Implement asynchronous loading for large assets
- Use streaming for audio/video when appropriate
- Implement progressive loading for large textures
Platform Compatibility
- Handle platform-specific file format requirements
- Implement fallback loading mechanisms
- Consider thread safety for multi-threaded platforms
Memory Management
- Monitor memory usage of cached assets
- Implement automatic cache cleanup
- Provide manual unloading API for memory-constrained environments
Asset Versioning
- Implement asset versioning to handle updates
- Consider cache invalidation strategies
- Support hot-reloading during development
By following these implementation guidelines, you can create a robust asset management system that provides functionality similar to OpenFL's built-in system while giving you complete control over the asset bundling and loading process.