Skip to main content

Asset Gates And Bitmap Cache

Hs2dAssetGate is the build-time readiness check for bitmap assets. GameBitmapCache is the shared bitmap loader/cache behind that gate. Use them together so scenes do not build worlds with missing art and do not reopen bitmap files from frame-time draw paths.

Source References

SourceWhat to read there
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dAssetGate.tsGate API, missing-name polling, status text, and requireBitmap.
../games/hosanna-ui/src/hosanna-game/assets/GameBitmapCache.tsBitmap loader interface, Roku default loader, getOrCreate, preload, and dispose.
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dLevelScene.tsLoading template: poll assets, build the world, then run update/render hooks.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.tsReal usage with deferred bundle readiness, asset gate registration, loading status, world build, and sprite pools.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/hosanario/HosanarioLevelScene.tsPlatformer example that registers a larger bitmap set before Hs2dWorldBuilder.fromLevel.

GameBitmapCache currently lives in src/hosanna-game/assets/GameBitmapCache.ts.

The Objects

ObjectResponsibility
GameBitmapCacheOwns bitmap references by URI, de-duplicates preload calls, and creates bitmaps through a loader.
GameBitmapLoaderPlatform-specific loader contract with getOrCreate(uri) and preload(uri).
RokuGameBitmapLoaderDefault loader. It creates roBitmap from a URI and returns undefined on failure.
Hs2dAssetGateOwns named bitmap requirements for a scene build and polls the cache until every name resolves.

The gate stores bitmaps by logical names such as ship, tile-atlas, or space-sky. The cache stores bitmaps by URI. Several gate names can point at the same URI, but most scenes should keep a one-to-one mapping for clarity.

Naming Contract

Asset docs use three different names. Keep them distinct:

NameExampleWhere it is used
Manifest keymy-game.images.shipasset-bundle.json and resolveGameAssetBundleUri.
Fallback URIpkg:/asset-bundles/my-game/images/ship.pngPackage/local fallback when the bundle state cannot resolve a key yet.
Asset-gate nameshipHs2dAssetGate.add(name, uri), Tiled hs2d:asset, tilesetAsset, decor frame mappings, and world-builder entity asset.

The manifest key resolves a URI. The URI opens a bitmap. The gate name is the stable in-engine name for that bitmap.

const shipUri = this.resolveBundleUri(
'my-game.images.ship',
'pkg:/asset-bundles/my-game/images/ship.png',
);

assetGate.add('ship', shipUri);

If a Tiled image layer has hs2d:asset=space-sky, register assetGate.add('space-sky', spaceSkyUri). If an entity binding says asset: 'enemy-atlas', register assetGate.add('enemy-atlas', enemyAtlasUri). Do not put a namespaced manifest key in Tiled unless you also used that exact string as the gate name.

Lifecycle

Asset lifecycle from namespaced bundle manifest keys through resolved URIs, short gate names, bitmap-cache polling, retained world construction, restart reuse, and safe disposalAsset lifecycle from namespaced bundle manifest keys through resolved URIs, short gate names, bitmap-cache polling, retained world construction, restart reuse, and safe disposal

The vertical shooter deliberately defers level, camera, and gate setup until the native-shoot-em-up bundle is ready. Pressing Start early therefore shows a loading status instead of trying to read empty or stale fallback paths.

Gate API

const assetGate = new Hs2dAssetGate({ bitmapCache: this.bitmapCache });

assetGate
.add('tile-atlas', this.tileAtlasUri)
.add('enemy-atlas', this.enemyAtlasUri)
.add('ship', this.shipUri)
.add('projectile', this.projectileUri);

if (!assetGate.poll()) {
this.setLoadingStatus(assetGate.getStatusText());
return undefined;
}

const ship = assetGate.requireBitmap('ship');
APIUse
new Hs2dAssetGate({ bitmapCache })Share an existing cache; if omitted, the gate creates its own cache.
add(name, uri)Register one named bitmap requirement.
addAll(urisByName)Register a map of name to URI.
poll()Try to resolve missing bitmaps through the cache. Returns true once all are ready.
isReadyRead the last readiness result.
getBitmap(name)Return a ready bitmap or undefined.
requireBitmap(name)Return a ready bitmap or throw Hs2dAssetGate asset not ready: <name>.
getMissingNames()Inspect the current missing-name set.
getStatusText()Loading text such as loading assets: ship, projectile.
cacheAccess the underlying GameBitmapCache.

poll() only revisits names that are still missing. It updates status text only when the missing-name set changes.

Bitmap Cache API

const cache = new GameBitmapCache();

const bitmap = cache.getOrCreate(uri);
const sameBitmap = cache.get(uri);

cache.preload(uri).then((loadedBitmap) => {
this.loadedBitmap = loadedBitmap;
});

cache.preloadAll([tileAtlasUri, shipUri, projectileUri]).then((bitmaps) => {
this.preloadedBitmaps = bitmaps;
});

cache.dispose();

preload and preloadAll return HsPromise. Shared Roku-transpiled game code should use HsPromise chaining or the level-scene polling/gate pattern, not async/await.

APIUse
get(uri)Return a cached bitmap without loading.
getOrCreate(uri)Return cached bitmap or synchronously ask the loader to create one.
preload(uri)Asynchronously load and cache one bitmap. Multiple calls share the same promise while loading.
preloadAll(uris)Load a list of URIs.
dispose()Replace the bitmap and loading records so held bitmap references can be released.
GameBitmapCache.setDefaultLoader(loader)Swap the process default loader for web/test platforms.

Failed getOrCreate calls are retried on every call by design because gates poll until assets appear. Do not put unguarded getOrCreate calls in draw loops for possibly missing art. If a frame-time path must reference optional art, add its own backoff or gate the art first.

World Build Pattern

Use this shape for Hs2dLevelScene subclasses:

private readonly bitmapCache = new GameBitmapCache();
private assetGate?: Hs2dAssetGate;

private tryInitializeAssets(): boolean {
if (this.assetGate) return true;
if (this.getBundleState() !== 'ready') {
this.setLoadingStatus('waiting for asset bundle my-game...');
return false;
}

const playerUri = this.resolveBundleUri(
MY_GAME_ASSETS.playerBundleKey,
MY_GAME_ASSETS.player,
);

this.assetGate = new Hs2dAssetGate({ bitmapCache: this.bitmapCache })
.add('player', playerUri)
.add('tile-atlas', this.resolveBundleUri(
MY_GAME_ASSETS.tileAtlasBundleKey,
MY_GAME_ASSETS.tileAtlas,
));

return true;
}

protected override buildWorld(): Hs2dWorld | undefined {
if (this.world) return this.world;
if (!this.tryInitializeAssets()) return undefined;

const assets = this.assetGate as Hs2dAssetGate;
if (!assets.poll()) {
this.setLoadingStatus(assets.getStatusText());
return undefined;
}

const built = Hs2dWorldBuilder.fromLevel({
level: this.loadedLevel as Hs2dLevel,
assets,
viewportWidth: this.screenWidth,
viewportHeight: this.screenHeight,
hud: this,
});

this.world = built.world;
return this.world;
}

If the scene also creates manual sprite pools, keep using the same cache after the gate is ready:

const ship = this.bitmapCache.getOrCreate(this.shipUri);
const projectile = this.bitmapCache.getOrCreate(this.projectileUri);
if (!ship || !projectile) return;

this.shipSpritePool = spriteLayer.addSpritePool({
id: 'my-game:ship',
capacity: 1,
definition: {
source: ship,
frames: [{ id: 'ship', x: 0, y: 0, width: 96, height: 128 }],
},
initialFrameId: 'ship',
anchor: { x: 48, y: 64 },
zIndex: 120,
offscreenX: -1000,
offscreenY: -1000,
});

This mirrors the vertical shooter: Hs2dWorldBuilder.fromLevel receives the ready gate for level art, and later sprite-pool construction reuses GameBitmapCache for ship/projectile sources.

Loading And Failure Behavior

Use clear loading statuses:

if (bundleState !== 'ready') {
if (bundleState.indexOf('failed') === 0) {
this.setLoadingStatus('asset bundle my-game ' + bundleState + ' - check the asset server');
} else {
this.setLoadingStatus('waiting for asset bundle my-game (' + bundleState + ')...');
}
return false;
}

When the bundle is ready but bitmaps are not, report the gate status:

if (!assetGate.poll()) {
this.setLoadingStatus(assetGate.getStatusText());
return undefined;
}

This separates descriptor/ensure failures from bitmap-open failures. That distinction matters on device: a ready manifest can still point at a bitmap path that fails to open.

Dispose And Restart

Restarting a level should usually reuse the existing cache. The scene has already paid the bitmap-open cost, and the restart should reset gameplay state rather than reload art.

Use bitmapCache.dispose() when the owner is long-lived and you need to explicitly release cached bitmap references after leaving a game or clearing a large art set. dispose() replaces the internal records instead of deleting keys one by one because dynamic-key deletion has been unreliable on device.

Do not call dispose() while any live world, sprite pool, tile layer, or cached surface still uses those bitmap references.

Do And Don't

DoDon't
Build gates after the bundle state is ready.Register gates with empty strings or unresolved keys and hope the paths fix themselves later.
Poll the gate from the loading/build path and return undefined until ready.Call requireBitmap before poll() returns true.
Share one cache between the gate and manual sprite pools in a scene.Create separate caches for the same art inside one world build.
Reuse cached bitmaps on level restart.Reopen roBitmap sources in every frame.
Use getStatusText() for loading UI and logs.Show a blank screen while assets are missing.
Dispose only when the owner can safely release all bitmap references.Dispose while a world that uses those bitmaps is still alive.
Talk to us