Skip to main content

Entity Bindings Decor And Zones

Tiled object groups become one of three things in the world-builder flow:

Object group roleBuilt output
hs2d:role=entities or no roleCode-bound sprite groups keyed by exported object type.
hs2d:role=decorParallax sprite decor, usually background or foreground decals.
hs2d:role=zonesData-only rectangles queried by gameplay code.

Unknown roles are also data-only. This lets a map carry editor-only or game-specific object groups without forcing renderer behavior.

Entity Binding Model

An entity object is a Tiled object whose exported JSON type is matched against the entities option passed to Hs2dWorldBuilder.fromLevel.

entities: {
enemy: {
capacity: 24,
asset: 'enemy-atlas',
frames: [
{ id: 'enemy-0', x: 0, y: 0, width: 128, height: 128 },
{ id: 'enemy-1', x: 128, y: 0, width: 128, height: 128 },
],
initialFrameId: 'enemy-0',
boundsWidth: 128,
boundsHeight: 128,
anchor: { x: 64, y: 64 },
zIndex: 90,
collisionBridge: this.collisionBridge,
spawn: (object, sprite, index, level) => this.spawnEnemy(object, sprite, index, level),
},
}

Binding fields:

FieldRequiredBehavior
capacityyesPhysical sprite budget for the group. The group virtualizes placed sprites over this capacity. Size it for worst visible count when zoom-out can reveal many objects.
assetyesAsset-gate bitmap name for the atlas.
framesyesSprite frame definitions for the atlas.
initialFrameIdyesFrame set before spawn runs.
layernoNamed sprite layer id. If omitted, the object group's hs2d:spriteLayer or builder default is used.
boundsWidth, boundsHeightnoSprite bounds.
anchornoSprite anchor.
zIndexnoEntity group z. Overrides object-group hs2d:z.
collisionBridgenoCollision bridge for the sprite group.
spawnnoBuild-time callback for each placed object. Copy map data into scene-owned state here.

The builder creates a group id of hs2d-entities:<type>, creates one sprite per bound object, sets the initial frame, positions it at object.x/object.y, makes it visible, and then calls spawn.

Objects with no binding are skipped and their type is added to built.unboundEntityTypes once. That is diagnostic information, not a fatal error.

Spawn Callback Pattern

Use spawn to bridge static map placement to runtime simulation arrays. The vertical shooter reads enemy objects once, stores simulation state in this.enemies, and then uses spawn to connect each built sprite with that state:

private createEnemies(level: Hs2dLevel): void {
const enemyObjects = level.getObjectsByType('enemies', 'enemy');
for (let i = 0; i < enemyObjects.length; i++) {
const object = enemyObjects[i];
const variant = level.getNumberProperty(object, 'variant', i % 4);
const health = level.getNumberProperty(object, 'health', this.getEnemyMaxHealth(variant));
this.enemies.push({
worldX: object.x,
worldY: object.y,
variant,
health,
maxHealth: health,
isDestroyed: false,
respawnMs: 0,
});
}
}

private spawnEnemy(object: Hs2dLevelObject, sprite: Hs2dLayerSprite, index: number): void {
const enemy = this.enemies[index];
if (!enemy) return;
sprite.setFrame('enemy-variant-' + enemy.variant);
sprite.setVisible(!enemy.isDestroyed);
sprite.setCollisionData({
kind: 'enemy',
entityId: 'my-game:enemy:' + object.id,
enemyIndex: index,
});
this.enemySprites.push(sprite);
}

After build, update scene-owned state and synchronize sprites:

private updateEnemies(deltaMs: number): void {
for (let i = 0; i < this.enemies.length; i++) {
const enemy = this.enemies[i];
if (enemy.isDestroyed) continue;
this.enemySprites[i]?.setWorldPosition(enemy.worldX, enemy.worldY);
}
}

Do not re-read the object group every frame.

Capacity Sizing

capacity is the physical sprite budget. It can be lower than total placed objects when the sprite group can virtualize offscreen items, but it must be high enough for the maximum visible count in your camera envelope.

For games with extreme debug zoom-out, size from placed objects:

private getEntityCapacity(level: Hs2dLevel, objectType: string, minimum: number): number {
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);
}

For tightly scrolling games, use a smaller tuned capacity and monitor culling/visibility stats on device.

Named Sprite Layers

Object groups can set hs2d:spriteLayer, and bindings can set layer. In ordinary Hs2dWorldBuilder.fromLevel(...) use, both should refer to the builder's one configured foreground id:

const built = Hs2dWorldBuilder.fromLevel({
// ...
spriteLayerId: 'gameplay-foreground',
entities: {
enemy: {
// ...
layer: 'gameplay-foreground',
},
},
});

The builder creates a private world and lazily creates one default foreground sprite layer. Its public options do not accept an existing world or pre-created layer, so a separate custom named layer cannot be injected before map resolution. If an object group or binding names an id other than spriteLayerId, the builder throws:

Hs2dWorldBuilder sprite layer not found: <layerId>

Usually omit layer and hs2d:spriteLayer; otherwise point them at the configured spriteLayerId. Build the world imperatively when the game genuinely needs several independently configured sprite layers. The source error's advice to add a missing layer before building is not currently achievable through the public fromLevel options.

Decor Layers

Decor object groups use hs2d:role=decor.

{
"type": "objectgroup",
"name": "background-decals",
"properties": [{ "name": "hs2d:role", "type": "string", "value": "decor" }],
"objects": [{
"name": "moon",
"type": "decal",
"x": 179.12,
"y": 4915.2,
"properties": [
{ "name": "frame", "type": "string", "value": "moon" },
{ "name": "parallaxX", "type": "float", "value": 0.035 },
{ "name": "parallaxY", "type": "float", "value": 0.075 },
{ "name": "zIndex", "type": "int", "value": 12 }
]
}]
}

Build with a frame-to-asset map when frame names differ from asset-gate names:

const built = Hs2dWorldBuilder.fromLevel({
level,
assets: assetGate,
viewportWidth: this.screenWidth,
viewportHeight: this.screenHeight,
decorFrames: {
moon: 'space-moon',
station: 'space-station',
satellite: 'space-satellite',
},
});

If no mapping exists, the builder uses the decor frame value as the asset name. If the asset is missing from the gate, build fails with a decor-frame error.

Decor defaults:

ValueDefault source
frameObject frame, then object name, then object type.
parallaxXObject parallaxX, then group native parallaxx, then 1.
parallaxYObject parallaxY, then group native parallaxy, then 1.
zIndexObject zIndex, then group hs2d:z, then order * 10.
cullPaddingXObject cullPaddingX, then group hs2d:cullPaddingX, then 512.
cullPaddingYObject cullPaddingY, then group hs2d:cullPaddingY, then 512.

Use decor for non-interactive sprites. If a thing has health, collision, pickup state, animation state, or gameplay ownership, bind it as an entity instead.

Zones

Zones are object groups with hs2d:role=zones. The builder does nothing with them, so they stay available as level data:

const spawn = level.getObjectsByType('zones', 'player-spawn')[0];
const hazards = level.getObjectsByType('zones', 'hazard');

this.player.x = spawn.centerX;
this.player.y = spawn.centerY;

Use zones for:

  • player spawns
  • checkpoints and triggers when they do not need a sprite
  • camera bounds or volumes
  • hazard rectangles
  • scripted encounter areas
  • exits and transitions when another visible entity owns the art

If the zone affects many actors, copy it into an efficient scene-owned structure at build/init time. Do not repeatedly filter object arrays in hot loops.

Gameplay Object Properties

Game-owned object properties should be read in one place and copied into typed state:

const platformObjects = level.getObjectsByType('entities', 'platform');
for (const object of platformObjects) {
this.platforms.push({
id: object.id,
x: object.x,
y: object.y,
axis: level.getStringProperty(object, 'axis', 'horizontal'),
order: level.getNumberProperty(object, 'order', 0),
});
}

Keep property names stable. If a property becomes central to multiple games, document it as a game convention; do not assume the Hs2d builder understands it.

Do And Don't

Do:

  • use Tiled object type as the binding key
  • copy map data into typed scene/simulation structures before gameplay starts
  • use spawn for one-time sprite setup, frame selection, and collision metadata
  • check built.unboundEntityTypes in development logs
  • use decor only for non-interactive parallax sprites
  • use zones for data-only rectangles
  • use the builder's configured spriteLayerId for binding/group layer overrides

Don't:

  • put sprite atlas frame rectangles in Tiled object properties
  • create new entity bindings during the frame loop
  • scan level.getObjects(...) every frame for common gameplay checks
  • let Tiled object arrays become the source of mutable runtime state
  • use decor for collidable gameplay actors
  • rely on unknown hs2d:role values being rendered
  • point a builder binding at a distinct layer that cannot be injected through fromLevel

Source Reference

SourceConfirms
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorldBuilder.tsEntity binding interface, object-layer role handling, sprite group creation, spawn callback order, decor region resolution, named layer errors, and unbound diagnostics.
../games/hosanna-ui/src/hosanna-game/hosanna2d/level/Hs2dLevel.tsObject lookup, typed property helpers, and parallax decor spec construction.
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorldBuilder.test.tsTested entity binding, unbound entity types, virtualized capacity, decor asset failures, and invisible layer skips.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.tsEnemy object loading, entity binding, decor frames, collision metadata, and sprite synchronization.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/hosanario/HosanarioLevelScene.tsMulti-type entity binding, capacity sizing from placed objects, checkpoint/coin/platform binding, and runtime sprite sync.
../hosanna-ui-game-samples-public/asset-bundles/hosanario/levels/build_levels.pyGenerated entity and zone object groups, gameplay object properties, and canonical prepared map output.
Talk to us