Skip to main content

World Builder And Level Loading

Hs2dWorldBuilder.fromLevel is the preferred entry point for map-driven games. It reads a prepared Tiled level once, allocates layers and sprite groups, returns references to the built systems, and then the frame loop works from scene-owned state.

Do not scan Tiled objects or raw JSON during gameplay frames.

Runtime Flow

Prepared Tiled map flowing through level loading, bundle URI resolution, bitmap gating, world construction, retained outputs, and frame-time synchronizationPrepared Tiled map flowing through level loading, bundle URI resolution, bitmap gating, world construction, retained outputs, and frame-time synchronization

Hs2dLevelLoader uses ReadAsciiFile(uri) and parseTiledJsonLevel. It throws if the file is missing or empty. Hs2dLevelManager adds level definitions, asset-key resolution, and caching.

Level Manager

Use Hs2dLevelManager when levels live in asset bundles:

this.levelManager = new Hs2dLevelManager({
bundleId: MY_GAME_ASSETS.bundleId,
levels: {
1: {
assetKey: MY_GAME_ASSETS.level1BundleKey,
fallbackUri: MY_GAME_ASSETS.level1,
},
},
resolveAssetUri: (_bundleId, assetKey, fallbackUri) => this.resolveBundleUri(assetKey, fallbackUri),
});

For fixed local paths or tests, use uri directly:

const levelManager = new Hs2dLevelManager({
bundleId: 'my-game',
levels: {
1: { uri: 'pkg:/asset-bundles/my-game/levels/level-1.json' },
},
});

loadLevel(1) returns a cached Hs2dLevel after the first load.

Asset Gate

The builder requires a ready Hs2dAssetGate:

const assetGate = new Hs2dAssetGate({ bitmapCache: this.bitmapCache });
assetGate.add('tile-atlas', this.resolveBundleUri(MY_ASSETS.tileAtlasKey, MY_ASSETS.tileAtlas));
assetGate.add('sky', this.resolveBundleUri(MY_ASSETS.skyKey, MY_ASSETS.sky));
assetGate.add('enemy-atlas', this.resolveBundleUri(MY_ASSETS.enemyAtlasKey, MY_ASSETS.enemyAtlas));

Before building:

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

If fromLevel is called while assets are missing, it throws with the missing asset names.

Build A World

Minimal shape:

const built = Hs2dWorldBuilder.fromLevel({
level,
assets: assetGate,
viewportWidth: this.screenWidth,
viewportHeight: this.screenHeight,
cameraX: this.bootstrapCamera.x,
cameraY: this.bootstrapCamera.y,
scale: this.bootstrapCamera.scale,
clearColor: 0x000000ff,
spriteLayerId: 'my-game-foreground',
entities: {
enemy: {
capacity: 24,
asset: 'enemy-atlas',
frames: [
{ id: 'idle', x: 0, y: 0, width: 64, height: 64 },
{ id: 'hit', x: 64, y: 0, width: 64, height: 64 },
],
initialFrameId: 'idle',
zIndex: 90,
spawn: (object, sprite, index, level) => {
sprite.setFrame(level.getStringProperty(object, 'variant', 'idle'));
this.bindEnemy(index, object, sprite);
},
},
},
hud: this,
});

const world = built.world;
world.setCameraController(this.cameraController);
this.terrainLayer = built.tileLayers['terrain'];
this.enemyGroup = built.entityGroups['enemy'];
this.world = world;

The builder processes level.layerOrder in file order. A layer may supply explicit z values, but file order is still the construction order.

Builder Options

Common Hs2dWorldFromLevelOptions:

OptionDefaultPurpose
levelrequiredParsed Hs2dLevel.
assetsrequiredReady Hs2dAssetGate.
viewportWidth, viewportHeightrequiredScreen-space viewport dimensions.
cameraX, cameraY0Initial world camera position.
parallaxOriginX, parallaxOriginYcamera positionOrigin for parallax strip math.
scale1Initial world camera scale.
clearColor0x000000ff unless map property existsWorld clear color fallback.
spriteLayerIdhs2d-foregroundDefault foreground sprite layer id.
cullPadding128Default foreground sprite-layer culling padding.
zoomScaleRatio1Default foreground sprite-layer zoom behavior.
projectionScaleRatioengine layer defaultProjection scaling behavior for dynamic tiles/sprites.
minScale, maxScalelayer defaultsZoom envelope. minScale also affects auto tile mode selection.
isScaleFromTopLeftlayer defaultScale projection origin behavior.
tileLayerModesmap propertiesCode-side tile mode override by layer id.
tileLayerChunkSizesmap properties/default 512Code-side cached chunk size override by layer id.
offscreenX, offscreenY-1000Parking position for hidden sprites.
tilesetAssettileset nameAsset-gate name for the tileset bitmap.
worldArtScale1One-time build scale for tile/entity art.
decorFramesframe name maps to same asset nameDecor frame-to-asset map.
entities{}Object type to entity binding map.
hudnoneObject with drawHud(screen).

Built World Result

Hs2dBuiltWorld returns:

FieldUse
worldThe Hs2dWorld to update/render.
levelThe same Hs2dLevel, returned for convenience.
spriteLayerThe builder-created default foreground sprite layer, if one was needed.
tileLayersTile layer id to the builder's sprite-pool dynamic, static, or cached-chunk layer instance. The builder does not create ring-surface layers.
tileRegionsRegions sliced from the tileset.
decorGroupsDecor object group id to parallax sprite group.
entityGroupsEntity type to sprite group.
entitySpritesEntity type to created layer sprites.
unboundEntityTypesObject types placed in the map without code bindings. Diagnostics only.

Unbound entity types do not fail the build. They are useful during development to catch missing bindings.

Tile Layer Selection

Prefer hs2d:mode in Tiled when the map owns the rendering choice:

Builder and manual-world tile renderer choices for auto, dynamic sprite-pool, ring-surface, static, and cached modesBuilder and manual-world tile renderer choices for auto, dynamic sprite-pool, ring-surface, static, and cached modes

{ "name": "hs2d:mode", "type": "string", "value": "auto" }

Use code-side overrides when the same prepared map needs different behavior by platform or debug mode:

const built = Hs2dWorldBuilder.fromLevel({
level,
assets: assetGate,
viewportWidth: this.screenWidth,
viewportHeight: this.screenHeight,
tileLayerModes: { terrain: 'cached' },
tileLayerChunkSizes: { terrain: { width: 256, height: 256 } },
});

auto estimates the worst-case dynamic pool:

columns = floor(viewportWidth / max(minScale, 0.1) / tileWidth) + poolPadding * 2
rows = floor(viewportHeight / max(minScale, 0.1) / tileHeight) + poolPadding * 2

If columns * rows is greater than 4096, the builder uses cached chunks. Otherwise it uses the dynamic pooled tile layer.

Hs2dWorldBuilder never selects Hs2dRingTileLayer; that is a manual world.addTileLayer({ mode: 'dynamic', renderMode: 'ring-surface', ... }) path.

The builder also owns a private world and can lazily create only its configured spriteLayerId. Map hs2d:spriteLayer properties and entity binding layer overrides should be omitted or match that id. Use imperative world construction when several separately configured sprite layers are required.

Collision And Tile Queries

Use Hs2dLevel helpers from simulation/collision code:

const tileX = GameMath.floorToInt(projectile.x / level.tileWidth);
const tileY = GameMath.floorToInt(projectile.y / level.tileHeight);

if (level.isSolidTile(tileX, tileY, 'terrain')) {
this.deactivateProjectile(projectile);
}

For tile-specific logic:

const tileKind = level.getTileKinds('terrain')[tileY * level.width + tileX];
const tileProps = level.getTileProperties(tileKind);
const damage = level.getNumberProperty({ properties: tileProps }, 'damage', 0);

Keep this kind of query in bounded simulation work. Do not traverse full tile arrays every frame unless the array is small and the work is intentionally amortized.

Sample Pattern

The vertical shooter and Hosanario follow the same build contract:

  1. Wait for the asset bundle to be ready.
  2. Load the prepared level through Hs2dLevelManager.
  3. Register every bitmap needed by image layers, tile layers, decor, and entities on Hs2dAssetGate.
  4. Wait for assetGate.poll().
  5. Call Hs2dWorldBuilder.fromLevel.
  6. Store built.tileLayers['terrain'], built.entityGroups[...], and any sprite arrays needed by the simulation.
  7. Add camera controller, particles, extra sprite pools, and HUD.
  8. In the frame loop, update simulation state and synchronize scene-owned arrays into sprites.

Hosanario also sizes entity capacity from placed objects so debug zoom-out can show the whole level without visible entity capping:

private getEntityCapacity(objectType: string, minimum: number): number {
const level = this.loadedLevel;
if (!level) return minimum;
let count = 0;
for (const ref of level.layerOrder) {
if (ref.kind === 'object') {
count += level.getObjectsByType(ref.id, objectType).length;
}
}
return Math.max(minimum, count);
}

Do And Don't

Do:

  • build the world only after the level and asset gate are ready
  • keep map layer names stable and use them as code ids
  • store returned layer/group references after build
  • bind placed objects to scene-owned simulation arrays in spawn
  • use code-side tileLayerModes only for runtime/platform overrides
  • dispose worlds/bitmap caches when a scene owns large cached surfaces

Don't:

  • call fromLevel before assetGate.poll() returns true
  • create sprites or regions inside the frame loop
  • scan all Tiled object groups every frame
  • mutate prepared map JSON to tune per-platform renderer choices
  • ignore unboundEntityTypes during development

Source Reference

SourceConfirms
../games/hosanna-ui/src/hosanna-game/hosanna2d/level/Hs2dLevelLoader.tsFile loading behavior and empty-file error.
../games/hosanna-ui/src/hosanna-game/hosanna2d/level/Hs2dLevelManager.tsLevel definitions, asset-key/fallback resolution, and cache behavior.
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorldBuilder.tsBuilder options, build order, asset readiness, layer construction, tile mode selection, result shape, and defaults.
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorldBuilder.test.tsTested build behavior for layers, entity binding, tile modes, auto cached fallback, chunk overrides, invisible layers, and missing assets.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.tsComplete vertical shooter loading and builder pattern.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/hosanario/HosanarioLevelScene.tsComplete platformer loading, entity capacity sizing, builder, and runtime sprite sync pattern.
Talk to us