Skip to main content

Level Scene Lifecycle

Hs2dLevelScene is the canonical base class for gameplay scenes that render an Hs2dWorld. It owns the transition from loading to a ready retained world and keeps restart behavior inside the existing allocation envelope.

Use it for new world or level games unless you are writing a small menu, diagnostics surface, or custom engine test.

For the per-frame input, simulation, camera, sprite, particle, telemetry, HUD, and render hooks, see Level Scene Frame Hooks.

What The Base Class Owns

Hs2dLevelScene owns shared gameplay plumbing:

ConcernBase class behavior
Loading stateRuns loading input and optional asset polling until a world exists.
World referenceStores the active Hs2dWorld and exposes isReady plus getWorld().
Camera accessExposes activeCamera, cameraX, cameraY, screen-to-world helpers, and tickSceneCamera().
HUDOwns an Hs2dHud and connects it to the world's text renderer when ready.
AssetsAccepts an Hs2dAssetGate, bundle id, and bundle URI resolver.
AudioCarries an optional audio manager for level music and SFX.
TelemetryProvides phase timer helpers and once-per-window stats hooks.
ParticlesUpdates a particle layer with registered native emitters.
Render templateSeparates update hooks from world.render(screen).

Game authors fill in the hooks. The base class decides when the hooks run.

Loading Phase

While this.world is undefined, update() runs this template:

Hs2dLevelScene moving from loading through asset polling and world construction into ready frames, state-only restart, or scene exitHs2dLevelScene moving from loading through asset polling and world construction into ready frames, state-only restart, or scene exit

The important detail is that loading and world build are still part of update, not render. Render only draws the loading screen while the world is missing.

protected override onLoadingUpdate(deltaMs: number, inputs: GameInput[]): void {
void deltaMs;
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
if (input.release) continue;
if (input.isButton('back')) {
this.context.navigateToMenu();
return;
}
}
}

protected override drawLoadingStatus(screen: GameScreen): void {
screen.Clear(0x101a33ff);
this.hud.drawTextCentered(
screen,
this.statusText || 'Loading...',
Math.floor(this.screenWidth / 2),
Math.floor(this.screenHeight / 2),
0xffffffff,
this.hudFont
);
}

Loading input should be conservative. Do not run gameplay actions before the world, sprite pools, and camera exist.

Build The World Once

buildWorld() should create the world and return it only after all required assets are ready. Prefer Hs2dWorldBuilder.fromLevel() for Tiled maps.

protected override buildWorld(): Hs2dWorld | undefined {
if (!this.tryInitializeAssets()) {
return undefined;
}

const assets = this.assetGate;
if (!assets || !assets.poll()) {
if (assets) {
this.setLoadingStatus(assets.getStatusText());
}
return undefined;
}

const built = Hs2dWorldBuilder.fromLevel({
level: this.loadedLevel as Hs2dLevel,
assets,
viewportWidth: this.screenWidth,
viewportHeight: this.screenHeight,
cameraX: this.cameraX,
cameraY: this.cameraY,
clearColor: 0x101a33ff,
spriteLayerId: 'my-game-foreground',
hud: this,
});

const world = built.world;
world.setCameraController(this.cameraController);
this.foregroundLayer = built.spriteLayer;
this.playerSpritePool = this.createPlayerPool(this.foregroundLayer);
return world;
}

Do asset resolution, Tiled reads, pool creation, layer lookup, and text prewarm setup here or in helpers called by this method. Do not perform that work every frame.

World Ready Hook

Use onWorldReady(world) when setup needs the base class to assign this.world and connect the HUD text renderer first.

protected override onWorldReady(world: Hs2dWorld): void {
this.createFloatingScoreText(world.textRenderer);
this.tickSceneCamera(0);
this.updateSceneSprites();
this.statusText = 'ready';
}

The base class assigns this.world and connects the HUD renderer before calling this hook. Keep world-dependent camera ticks and initial synchronization here unless buildWorld() deliberately assigns this.world itself.

Restart

Pause menus and debug tools call restartLevel(). Override it to reset logical state, camera state, pools, counters, and synchronized sprites.

override restartLevel(): void {
this.playerX = this.startPlayerX;
this.playerY = this.startPlayerY;
this.score = 0;
this.playerHealth = 100;
this.projectiles.deactivateAll();
if (this.particles) {
this.particles.clear();
}
this.cameraX = this.startCameraX;
this.cameraY = this.startCameraY;
this.tickSceneCamera(0);
this.updateSceneSprites();
}

Restart should not rebuild the whole world unless the level definition or assets changed. Reset state and reuse existing pools.

Do And Don't

Do:

  • Extend Hs2dLevelScene for gameplay worlds.
  • Gate assets before building the world.
  • Build layers, pools, and Tiled bindings once.
  • Use onWorldReady() for setup that requires this.world.
  • Reset and reuse the existing world on restart.

Don't:

  • Load bitmaps, regions, levels, or manifests after entering ready gameplay.
  • Tick a world-dependent camera inside buildWorld() before the base class assigns this.world.
  • Rebuild the world for a state-only restart.
  • Open pause by replacing the level scene; push a pausing overlay instead.

Source References

SourceLifecycle facts to verify
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dLevelScene.tsLoading state, isReady, asset gate polling, world assignment, HUD hookup, world-ready hook, and restart hook.
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dScene.tsBase scene hooks inherited by level scenes.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.tsComplete level implementation: bundle readiness, world build, world-ready setup, restart, and disposal.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterPauseScene.tsHow pause overlays call back into restartLevel() and leave the level scene alive underneath.
Talk to us