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 use | Better 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
| Phase | Behavior |
|---|---|
| Update | Inherits the base no-op update. |
| Render | Inherits the base no-op render. |
| Compose | Calls 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
| Source | Why it matters |
|---|---|
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dCachedSurfaceLayer.ts | Hs2dCustomLayer implementation and per-frame compose callback contract. |
../games/hosanna-ui/src/hosanna-game/scene/GameSceneRenderer.ts | Base layer no-op update/render, render-frame order, and screen clear/compose phases. |
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorld.ts | addCustomLayer world API. |
docs/hosanna-game/performance-covenant.md | Rules a custom layer must still satisfy. |