Skip to main content

Assets And Asset Bundles

Game art, audio, fonts, and levels belong in asset bundles. This page covers bundle layout, manifests, key naming, and URI resolution. Bitmap readiness lives in Asset Gates And Bitmap Cache. Audio registration and playback lives in Audio Manager.

Source References

SourceWhat to read there
../games/hosanna-ui/src/hosanna-game/assets/GameAssetBundleManager.tsRuntime bundle descriptor loading, descriptor caching, URL normalization, and ready/failed states.
../games/hosanna-ui/src/hosanna-game/assets/GameAssetBundleTypes.tsPublic GameAssetBundleState shape passed into game contexts.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/assetBundles.tsSample-side key and fallback URI resolution helpers.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/NativeShootEmUpAssets.tsReference constants for bundle IDs, manifest keys, and pkg:/asset-bundles/... fallbacks.
../hosanna-ui-game-samples-public/asset-bundles/native-shoot-em-up/asset-bundle.jsonConcrete descriptor with image, audio, font, and level entries.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/ExampleGamesController.tsLoad game bootstrap, register bundle audio, and pass ready bundle states into game contexts.

Bundle Layout

Use one directory per game or shared art package:

asset-bundles/my-game/
asset-bundle.json
app.config.json
images/
audio/
fonts/
levels/

The vertical shooter reference bundle follows the same pattern:

asset-bundles/native-shoot-em-up/
asset-bundle.json
app.config.json
images/
audio/
fonts/
levels/

asset-bundle.json is the runtime descriptor. app.config.json is loaded separately by the examples shell when a game definition points at it.

Manifest Shape

Descriptors use a stable bundle ID and an assets array:

{
"bundleId": "native-shoot-em-up",
"appConfig": "/asset-bundles/native-shoot-em-up/app.config.json",
"assets": [
{
"key": "native-shoot-em-up.images.ship",
"url": "/asset-bundles/native-shoot-em-up/images/ship.png",
"fileName": "images/ship.png",
"required": false
},
{
"key": "native-shoot-em-up.audio.music",
"url": "/asset-bundles/native-shoot-em-up/audio/music.mp3",
"fileName": "audio/music.mp3",
"required": false
}
]
}
FieldPurpose
bundleIdLogical bundle name. It must match the game definition and the load request.
assets[].keyCode-facing stable key. Scenes should use constants for these.
assets[].urlDescriptor URL. Leading slash URLs are resolved against the bundle manager baseUrl.
assets[].fileNameBundle-local path used by packaged fallback logic and resolved manifests.
assets[].requiredPassed through to the bridge asset bundle task manager.
assets[].headersOptional request headers, also passed through when present.

Do not replace missing art with generated rectangles or text in shared game code. Add the asset to the bundle and let the normal bundle, gate, and cache pipeline expose the failure.

Key Idioms

Use namespaced keys that start with the bundle ID:

my-game.images.player
my-game.images.tile-atlas
my-game.audio.music
my-game.audio.hit
my-game.fonts.hud
my-game.levels.level-1

This convention keeps keys unique when multiple bundles are loaded into the same game context. It also lines up with default music lookup: the examples controller first tries <bundleId>.music, then <bundleId>.audio.music.

Keep code-side constants beside the game-specific asset roots:

const ASSET_ROOT = 'pkg:/asset-bundles/my-game/';
const IMAGE_ROOT = `${ASSET_ROOT}images/`;
const AUDIO_ROOT = `${ASSET_ROOT}audio/`;

export const MY_GAME_ASSETS = {
bundleId: 'my-game',
player: `${IMAGE_ROOT}player.png`,
playerBundleKey: 'my-game.images.player',
music: `${AUDIO_ROOT}music.mp3`,
musicSoundKey: 'my-game.audio.music',
} as const;

The key and fallback URI should travel together. That makes the code robust across packaged runs, local web runs, and downloaded/ensured bundle paths.

Loading States

GameAssetBundleManager.loadBundleState(bundleId, version?) builds this descriptor URL:

<baseUrl>/asset-bundles/<bundleId>/asset-bundle.json

It returns a state object:

StateMeaning
loadingThe descriptor or asset ensure work is active.
readyThe manifest is ready and can resolve assets.
failedDescriptor fetch or asset ensure failed. Inspect error and bundle logs.

The manager first tries a cached descriptor stored under the bundle cache directory as _bundle.json. If the cached descriptor has the wrong bundleId or requested version, it is ignored or treated as stale. Stale descriptors force a new descriptor fetch before asset ensure.

When the descriptor is fetched, leading slash asset URLs are normalized against the manager baseUrl, then passed to AssetBundleTaskManager.ensureBundle with maxConcurrent: 2. Ready manifests log ready and failed key counts; failed keys log the per-asset error when available.

Fallback URI Pattern

Every scene should resolve by manifest key and keep a package URI fallback:

const playerUri = resolveGameAssetBundleUri(
context,
MY_GAME_ASSETS.bundleId,
MY_GAME_ASSETS.playerBundleKey,
MY_GAME_ASSETS.player,
);

resolveGameAssetBundleUri checks context.assetBundles for a matching ready bundle. It supports both descriptor-style asset arrays and ready manifest records:

  • Legacy descriptor arrays return asset.url for a matching asset.key.
  • Ready manifest records return asset.path when the record is state: 'ready'.
  • If no ready asset is found, the fallback is returned.

Use resolveGameAssetUri when the caller already has a pkg:/asset-bundles/<bundleId>/<relativePath> URI:

const courseUri = resolveGameAssetUri(
context,
'pkg:/asset-bundles/mini-golf/levels/course-1.json',
);

That helper parses the bundle ID and relative path, then matches manifest entries by url or fileName. It returns the prepared asset path when a ready manifest has one; otherwise it returns the original URI.

Runtime Flow

Asset bundle descriptor and ensure flow leading to resolved scene URIs, bitmap readiness gates, retained world construction, and safe cache reuseAsset bundle descriptor and ensure flow leading to resolved scene URIs, bitmap readiness gates, retained world construction, and safe cache reuse

Scene code should still handle not-loaded, loading, and failed bundle states. The vertical shooter defers level, camera, and gate initialization until the native-shoot-em-up bundle reports ready; if the bundle fails, it shows a terminal loading status instead of trying to read empty fallbacks.

Do And Don't

DoDon't
Use namespaced keys such as native-shoot-em-up.images.ship.Use bare keys like ship in shared or multi-bundle contexts.
Keep manifest keys and pkg:/asset-bundles/... fallbacks in constants.Scatter string literals through scenes.
Resolve through resolveGameAssetBundleUri or resolveGameAssetUri.Assume asset.url is the final runtime path after bundle ensure.
Wait for the relevant bundle state before loading levels or registering bitmap gates.Build an Hs2d world while bundle state is still loading.
Let the manifest expose missing files and failed keys.Generate placeholder art in runtime code to hide missing bundle entries.
Register audio after bundle states are available.Create audio resources directly in gameplay loops.
Talk to us