Dynamic Tile Layer
Dynamic tiles have two retained implementations:
Hs2dTileLayerkeeps a fixed pool of compositor sprites for the visible camera window plus padding.Hs2dRingTileLayerkeeps a bounded tile surface and repaints only ring slots exposed as the camera crosses tile boundaries.
Both avoid raw per-frame full-map drawing and expose tile statistics. Choose explicitly for manual worlds; the Tiled world builder currently creates the sprite-pool implementation for its dynamic and auto modes.
When To Use
Use a dynamic tile layer when:
| Good fit | Better alternative |
|---|---|
| The map scrolls and the visible tile pool is modest. | Cached chunk tile when zoom-out or small tiles would require too many visible sprites. |
| Camera movement benefits from row/column repaint and a bounded surface. | Ring surface, provided the visible ring fits the platform surface-size limit. |
| Tile contents are static but the camera moves frequently. | Static tile when the whole map is small enough to cache into one surface. |
| You need per-tile compositor sprites in a foreground layer. | Cached surface for non-tile custom terrain. |
Hs2dWorldBuilder auto mode keeps dynamic rendering while the worst-case pool is at or under the configured threshold, then switches to cached chunks when the pool would be too large.
Construction Pattern
Manual worlds need a host sprite layer:
const tileHost = world.addHs2dSpriteLayer({
id: 'terrain-sprites',
width: screenWidth,
height: screenHeight,
clearColor: 0x00000000,
zoomScaleRatio: 0,
minScale: 1,
maxScale: 1,
offscreenX: -1000,
offscreenY: -1000,
});
const terrain = world.addTileLayer({
id: 'terrain',
mode: 'dynamic',
layer: tileHost,
tileWidth: 32,
tileHeight: 32,
worldColumns: level.width,
worldRows: level.height,
tileKinds,
regions: tileRegions,
minScale: 0.8,
poolPadding: 2,
zIndex: 0,
offscreenX: -1000,
offscreenY: -1000,
});
For a ring surface, no host sprite layer is required:
const terrain = world.addTileLayer({
id: 'terrain',
mode: 'dynamic',
renderMode: 'ring-surface',
tileWidth: 32,
tileHeight: 32,
worldColumns: level.width,
worldRows: level.height,
tileKinds,
regions: tileRegions,
minScale: 0.8,
poolPadding: 1,
clearColor: 0x0f172aff,
});
Tiled:
Tile layer: terrain
hs2d:mode = dynamic
hs2d:poolPadding = 2
hs2d:z = 20
hs2d:mode=auto can also resolve to the dynamic sprite-pool path. renderMode: 'ring-surface' is currently a manual Hs2dWorld.addTileLayer() option, not a Tiled property consumed by Hs2dWorldBuilder.
Update, Render, And Compose Behavior
| Phase | Behavior |
|---|---|
| Build | Allocates tile sprites on the host layer compositor. The pool is sized from viewport, tile size, padding, and minScale. |
| World update | Hs2dWorld.update calls tileLayer.update(camera) before render. |
| Tile update | Computes the first visible cell, active columns and rows, recycles entries whose world cell changed, swaps regions only when needed, parks empty cells, and moves drawable tiles. |
| Render/compose | The host sprite layer renders and composes the tile sprites with its other compositor sprites. |
Stats from getStats() include pool size, visible tiles, recycled tiles, region changes, drawable changes, and moved tiles.
For the ring implementation, update shifts the retained logical ring and directly repaints entering rows and columns. Compose crops up to four wrapped regions from its surface. Ring stats report repaints, paints, clears, composed regions, and fallback strips.
preparationTileBudget is a deprecated, ignored compatibility option. The retained preparedTiles, preparedStripBlits, deadlinePreparedTiles, and preparationTileBudget stats are always zero, and preparationEnabled is always false. Do not build diagnostics or tuning controls around those fields.
Performance Notes
- Pass the smallest supported camera scale as
minScaleso the renderer is sized before gameplay. - Keep tile size large enough that the visible pool remains within budget.
- Use
poolPaddingsparingly; it prevents edge popping but increases pool and loop size. - Treat zoom below the declared
minScaleas unsupported. The sprite-pool path can cold-rebuild and grow; the ring path sizes its bounded surface from the declared minimum rather than the current smaller scale. - Use cached chunks for large sparse maps or very small tiles.
- Keep tile data in arrays and regions; do not query raw Tiled JSON during frames.
Code Sample
protected override onUpdate(deltaMs: number, inputs: GameInput[]): void {
this.updatePlayer(deltaMs, inputs);
this.tickSceneCamera(deltaMs, this.mapZoom);
}
protected override updateSceneSprites(): void {
const stats = this.terrainLayer?.getStats();
this.visibleTiles = stats?.visibleTiles ?? 0;
}
The actual tile movement happens inside Hs2dTileLayer.update(camera), called by the world. Game code reads stats or changes camera state; it does not draw tiles.
Source References
| Source | Why it matters |
|---|---|
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dTileLayer.ts | Dynamic tile pool sizing, active window, recycling, projection scale, stats, and hot path. |
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dRingTileLayer.ts | Ring layout, entering-cell repaint, wrapped crop composition, tile mutation, and ring stats. |
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorld.ts | addTileLayer dynamic mode and world tile update loop. |
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorldBuilder.ts | Tiled hs2d:mode, auto-mode threshold, poolPadding, hs2d:spriteLayer, and z mapping. |
../hosanna-ui-game-samples-public/src/hosanna-game-examples/diagnostics/H2dTilemapModeDemoController.ts | Cached chunks versus dynamic ring-surface diagnostic with camera, zoom, and repaint telemetry. |
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.ts | Example map-driven terrain layer and stats usage in gameplay. |