Skip to main content

Text, HUD, And Floating Labels

Text is part of the Hs2d rendering system. Gameplay HUD and floating labels should use cached text, bitmap text, text sprite pools, or floating text systems instead of rebuilding text surfaces every frame.

Source References

SourceWhy it matters
../games/hosanna-ui/src/hosanna-game/core/Hs2dTextCache.tsBitmap cache, cache keys, fallback direct draw, padding, and redraw behavior.
../games/hosanna-ui/src/hosanna-game/core/Hs2dTextRenderer.tsDirect text, cached bitmap text, prewarming, sprite text creation, floating text creation, and renderer stats.
../games/hosanna-ui/src/hosanna-game/core/Hs2dCachedTextRenderer.tsLightweight cached text wrapper used by diagnostics and older examples.
../games/hosanna-ui/src/hosanna-game/core/Hs2dTextSprite.tsMovable cached text sprite with screen/world positioning and camera projection.
../games/hosanna-ui/src/hosanna-game/core/Hs2dTextSpritePool.tsFixed-capacity pool of text sprite flyweights.
../games/hosanna-ui/src/hosanna-game/core/Hs2dFloatingTextSystem.tsPooled floating labels with velocity, lifetime, fade color, world/screen coordinate space, and culling.
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dLevelScene.tsHUD text renderer hookup and drawCachedText helper.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.tsReference HUD, gameplay cache prewarm, world-space floating score labels, and stats-window debug text.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/diagnostics/TextRigController.tsDiagnostic text stats, repeated bitmap cache samples, and layer-local static bitmap text.

Text Options

APIUse
Hs2dLevelScene.drawCachedText / Hs2dHud.drawCachedTextStable HUD labels such as score, lives, wave, timer, and ammo.
Hs2dTextRenderer.drawTextDirect text for loading screens, diagnostics, and small temporary overlays.
Hs2dTextRenderer.drawBitmapText / drawCachedBitmapTextCached bitmap text through the world text renderer.
Hs2dTextRenderer.prewarmBitmapTextPre-create common labels or digits before gameplay uses them.
Hs2dTextSpriteA movable cached text sprite.
Hs2dTextSpritePoolFixed-capacity pool of text sprite flyweights.
Hs2dFloatingTextSystemScore/combo/damage labels with pooled entities, pooled text sprites, lifetime, velocity, fade, and culling.
Hs2dCachedTextRendererSimple cached text for menus, diagnostics, and older examples.

Cache Model

Hs2dTextCache stores a bitmap per cache key. A cached draw:

  1. Looks up cacheKey.
  2. Reuses the bitmap when text, color, and font match.
  3. Re-renders the bitmap when content changes.
  4. Draws the bitmap with DrawObject.
  5. Falls back to direct DrawText if a bitmap cannot be created.

Stable cache keys matter. Use keys that identify the UI slot or reusable label family:

this.drawCachedText(screen, 'hud:score', `SCORE ${this.score}`, 36, 28, 0xfef08aff, this.titleFont);
this.drawCachedText(screen, 'hud:hp', `HP ${this.shipHealth}`, this.screenWidth - 170, 58, 0xa7f3d0ff, this.hudFont);

For repeated labels, include text and color:

const cacheKey = 'score:' + text + ':' + color;

Do not create unique keys per frame. That turns a cache into an unbounded bitmap factory.

HUD Text

Hs2dLevelScene owns an Hs2dHud. When the world is ready, the scene connects the HUD to the world's text renderer. Use drawCachedText in drawHud(screen).

override drawHud(screen: GameScreen): void {
this.drawCachedText(
screen,
'hud:score',
'Score ' + String(this.score),
32,
24,
0xffffffff,
this.hudFont,
);
}

Use stable cache keys. If the same label changes value, keep the same key and pass the new text. For long debug strings, update the stored string once per stats window and draw that stored string from drawHud.

private hudStatusText = 'FPS 0  T 0  E 0  P 0';

override drawHud(screen: GameScreen): void {
this.drawCachedText(screen, 'hud:status', this.hudStatusText, 36, 58, 0x55ff99ff, this.hudFont);
}

protected override onStatsWindow(): void {
const nextText = `FPS ${this.fps} T ${this.visibleTiles} E ${this.visibleEnemies} P ${this.activeProjectiles}`;
if (nextText !== this.hudStatusText) {
this.hudStatusText = nextText;
}
}

The vertical shooter intentionally keeps its score text simple, but its debug HUD text follows this throttled snapshot pattern.

Prewarming

Prewarm common labels before gameplay needs them. This moves bitmap creation and text rasterization out of combat spikes.

private prewarmGameplayTextCache(screen: GameScreen): void {
if (this.hasPrewarmedGameplayTextCache) return;
const renderer = this.activeTextRenderer;
if (!renderer) return;

this.hasPrewarmedGameplayTextCache = true;

const items: Hs2dTextPrewarmItem[] = [
{ text: '+20', color: 0xfef08aff, font: this.titleFont, cacheKey: 'score:+20:4277177087' },
{ text: '+100', color: 0xfbbf24ff, font: this.titleFont, cacheKey: 'score:+100:4223608063' },
{ text: '+500', color: 0x67e8f9ff, font: this.titleFont, cacheKey: 'score:+500:1743321599' },
];

renderer.prewarmBitmapText(screen, items);
}

Call prewarm from a bounded place, such as the first HUD draw after the world text renderer exists, or during a loading/build step where a screen target is available.

Actual bitmap prewarming requires this explicit renderer call because rasterization needs a draw target. The similarly named prewarm option on text sprite pools only supplies initial text, color, and cache keys to invisible flyweights; it does not create bitmap cache entries.

Text Sprites

Use Hs2dTextSprite when text needs sprite-like movement:

  • setPosition(x, y) for screen-space labels
  • setWorldPosition(x, y) for world-space labels
  • setVisible(visible) to hide without destroying
  • setText(text) to update cached content
  • setColor(color) to update color
  • setCacheKey(key) when text/color changes
  • draw(target, camera) or drawAt(target, x, y) to draw cached text
const sprite = world.textRenderer.createSpriteText({
cacheKey: 'banner:ready',
text: 'READY',
x: 320,
y: 180,
color: 0xffffffff,
font: this.titleFont,
});

sprite.setWorldPosition(player.worldX, player.worldY - 48);
sprite.draw(screen, world.camera);

Hs2dTextSprite is a flyweight over cached text. Moving it does not redraw the bitmap. Changing text, color, or cache key can redraw.

Text Sprite Pools

Use Hs2dTextSpritePool when many labels can be active. It allocates all text sprite flyweights in the constructor and never creates/destroys sprites afterwards.

const pool = new Hs2dTextSpritePool(world.textRenderer, {
id: 'combo-labels',
capacity: 16,
font: this.titleFont,
cacheKeyPrefix: 'combo',
// Seeds invisible sprite values only; this does not rasterize bitmap text.
prewarm: [
{ text: '+100', color: 0xfef08aff },
{ text: '+500', color: 0x67e8f9ff },
],
});

world.textRenderer.prewarmBitmapText(screen, [
{ text: '+100', color: 0xfef08aff, font: this.titleFont, cacheKey: pool.getCacheKey('+100', 0xfef08aff) },
{ text: '+500', color: 0x67e8f9ff, font: this.titleFont, cacheKey: pool.getCacheKey('+500', 0x67e8f9ff) },
]);

The pool is allocation only. Its prewarm entries are seed values, not a rasterization pass, because construction has no text target and every flyweight starts invisible. Motion, lifetime, fade, reuse policy, and culling belong in a system such as Hs2dFloatingTextSystem.

Floating Labels

Use Hs2dFloatingTextSystem for score, combo, damage, and pickup labels. It combines a fixed entity pool with an Hs2dTextSpritePool.

The normal shape:

  1. Create the floating text system when the world is built.
  2. Explicitly prewarm common text and colors through Hs2dTextRenderer.prewarmBitmapText(target, items).
  3. Spawn labels from gameplay events.
  4. Tick the system from the scene update.
  5. Draw or sync through the world text renderer.

Keep the capacity explicit. The floating text system uses reuseOldestWhenFull, so a full pool reuses an existing label instead of allocating.

private createFloatingScoreTextPool(textRenderer: Hs2dTextRenderer): void {
if (this.floatingScoreTextPool) return;

this.floatingScoreTextPool = textRenderer.createFloatingTextSystem({
id: 'my-game:floating-score',
capacity: 16,
font: this.titleFont,
cacheKeyPrefix: 'score',
coordinateSpace: 'world',
cullBounds: {
width: this.screenWidth,
height: this.screenHeight,
paddingX: 120,
paddingY: 80,
},
});
}

private emitFloatingScoreText(points: number, x: number, y: number): void {
const pool = this.floatingScoreTextPool;
if (!pool) return;

const text = '+' + points;
const color = points >= 500 ? 0x67e8f9ff : 0xfef08aff;
const lifetimeMs = points >= 500 ? 1200 : 900;

pool.emitText({
text,
x,
y,
color,
velocityY: points >= 500 ? -128 : -96,
lifetimeMs,
fadeAfterMs: lifetimeMs * 0.66,
fadeColor: color & 0xffffff99,
});
}

protected override onUpdate(deltaMs: number, inputs: GameInput[]): void {
this.updateGameplay(deltaMs, inputs);
this.floatingScoreTextPool?.update(deltaMs);
}

override drawHud(screen: GameScreen): void {
this.floatingScoreTextPool?.draw(screen, this.activeCamera);
}

Use coordinateSpace: 'world' for labels attached to enemies, pickups, damage, or map events. Use coordinateSpace: 'screen' for UI feedback that should not move with the camera.

Set cullBounds for world labels so offscreen labels do not draw.

Direct DrawText

Direct DrawText is allowed for:

  • loading screens
  • diagnostics
  • bounded menus
  • debug overlays
  • temporary failure/status text before the world text renderer exists

New gameplay HUDs and floating labels should use cached text by default. Avoid building new strings and drawing raw text repeatedly for large numbers of labels. Direct text still has a place in diagnostics because it makes renderer stats visible and keeps debug scaffolding simple.

Renderer Stats

Hs2dTextRenderer.getStats() returns:

StatMeaning
directDrawsDirect DrawText calls.
bitmapDrawsCached bitmap text draw calls.
cacheHitsCached draws that reused an existing bitmap.
cacheRedrawsCached draws that re-rendered a bitmap.
fallbackDrawsCached draws that fell back to direct text.

Use this in diagnostics:

protected override onStatsWindow(): void {
const stats = this.world?.textRenderer.getStats();
this.statusText =
`text direct ${stats?.directDraws ?? 0}`
+ ` bitmap ${stats?.bitmapDraws ?? 0}`
+ ` hits ${stats?.cacheHits ?? 0}`
+ ` redraws ${stats?.cacheRedraws ?? 0}`;
this.world?.textRenderer.resetStats();
}

High cacheRedraws usually means unstable cache keys, changing text every frame, changing color every frame, or missing explicit renderer prewarm coverage. Supplying only a pool or floating-system prewarm option does not rasterize bitmaps.

Performance Rules

DoDon't
Prewarm common labels and colors with Hs2dTextRenderer.prewarmBitmapText(target, items).Assume a text-pool prewarm seed rasterizes invisible flyweights.
Use stable cache keys for HUD slots.Append frame counters or timestamps to cache keys.
Use text sprite pools or floating text systems for many labels.Allocate text sprites in the frame loop.
Update long debug HUD strings once per stats window.Rebuild multi-field debug strings inside drawHud every frame.
Use world-space floating text with cull bounds for gameplay labels.Draw offscreen world labels just because they are active.
Reset renderer stats after a stats window.Treat cumulative cache stats as a current-frame signal.
Use direct text for loading screens and diagnostics.Use direct text for high-volume gameplay labels.
Add real art for sprites.Use text as placeholder art for missing gameplay sprites.
Talk to us