Skip to main content

Custom Layer

Hs2dCustomLayer is the escape hatch. It is a GameSceneLayer whose compose(screen) method delegates to a callback supplied by game code. It exists for narrow cases that do not fit a built-in layer yet.

Custom layers must still obey the performance covenant. They are not permission to add raw gameplay draw loops. If a feature draws many sprites, particles, tiles, labels, or background elements, build or use a reusable Hs2d layer, pool, particle system, text system, or cached surface instead.

When To Use

Use a custom layer only when:

Acceptable useBetter alternative
Small bounded debug overlay that cannot use HUD.HUD layer for normal screen-space overlays.
Bridging an existing measured renderer during migration.Port to sprite pools, tile layers, particles, or cached surfaces.
A tiny compose callback around an already-cached bitmap/surface.Hs2dCachedSurfaceLayer for world-space cached content.

Do not use custom layers for enemies, bullets, tilemaps, particle effects, floating labels, or scrolling backgrounds.

Construction Pattern

world.addCustomLayer({
id: 'debug-crosshair',
compose: (screen) => {
if (!this.showDebugCrosshair) return;
screen.DrawRect(this.cx - 8, this.cy, 16, 1, 0xff0000ff);
screen.DrawRect(this.cx, this.cy - 8, 1, 16, 0xff0000ff);
},
});

Keep the callback stable and bounded. Avoid allocating objects or arrays inside it.

Update, Render, And Compose Behavior

PhaseBehavior
UpdateInherits the base no-op update.
RenderInherits the base no-op render.
ComposeCalls the supplied callback every frame at the layer's render-order position.

The callback receives the final GameScreen, not a layer compositor surface. It runs after the screen clear and after any earlier layers have composed.

Performance Notes

  • Keep the callback O(1) or bounded by a very small constant.
  • Do not allocate in the callback.
  • Do not query Tiled data, entity lists, collision systems, or assets from the callback.
  • If the callback loops over gameplay objects, move that work to a sprite pool or cached layer.
  • If the callback draws static content, use a cached surface.
  • Add a real layer type once the same custom pattern appears in more than one place.

Code Sample

const cursor = { x: 0, y: 0 };

world.addCustomLayer({
id: 'cursor-debug',
compose: (screen) => {
if (!this.debugCursorEnabled) return;
screen.DrawRect(cursor.x - 5, cursor.y, 11, 1, 0xffffffff);
screen.DrawRect(cursor.x, cursor.y - 5, 1, 11, 0xffffffff);
},
});

protected override onUpdate(_deltaMs: number, _inputs: GameInput[]): void {
cursor.x = Math.round(this.activeCamera.x);
cursor.y = Math.round(this.activeCamera.y);
}

Source References

SourceWhy it matters
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dCachedSurfaceLayer.tsHs2dCustomLayer implementation and per-frame compose callback contract.
../games/hosanna-ui/src/hosanna-game/scene/GameSceneRenderer.tsBase layer no-op update/render, render-frame order, and screen clear/compose phases.
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorld.tsaddCustomLayer world API.
docs/hosanna-game/performance-covenant.mdRules a custom layer must still satisfy.
Talk to us