Files
2026-05-27 23:29:53 +01:00

4.1 KiB

OpenSpec Change: enterprise-gaming-game-loop

  • Change ID: enterprise-gaming-game-loop
  • Package: @suaveplan/game-loop
  • Category: gaming/engine
  • Target path: gaming/engine/game-loop
  • Status: PROPOSED
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/game-loop.md

Summary

Deliver @suaveplan/game-loop on the genesis branch from scratch at enterprise quality. The package provides a fixed-timestep game loop with tick scheduling — the heartbeat that drives @suaveplan/ecs world updates and all time-sensitive gameplay systems. The loop implements the classic "fix your timestep" accumulator pattern: unlimited render interpolation alpha with a capped fixed physics step, lag compensation, frame-time smoothing, and panic detection. Genesis adds structured errors, injectable clock and AbortSignal cancellation, OTel frame-time histograms, and deterministic test harness via FakeClock.


Current-state audit (genesis)

Package does not exist on genesis yet — fresh build.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) GameLoopError; GAME_LOOP.* error codes
@suaveplan/types (workspace:^) Clock injectable
@suaveplan/events (workspace:^) loop:tick, loop:panic, loop:stop events
zod (catalog:forms) GameLoopOptions schema
@opentelemetry/api (catalog:observability) Frame-time histogram, tick counter

Public API surface

function createGameLoop(options: GameLoopOptions): GameLoop

interface GameLoopOptions {
  fixedStep: number           // ms per physics tick, e.g. 16.666
  maxFrameTime?: number       // ms cap to prevent spiral of death, default 250
  onTick: (dt: number) => void
  onRender?: (alpha: number) => void
  clock?: Clock
  logger?: LoggerLike
}

interface GameLoop {
  start(signal?: AbortSignal): void
  stop(): void
  readonly isRunning: boolean
  readonly tickCount: number
  readonly fps: number           // rolling average over last 60 frames
  readonly frameBudgetMs: number
}

// Scheduler — attach one-shot or recurring callbacks to loop ticks
function createTickScheduler(loop: GameLoop): TickScheduler

interface TickScheduler {
  scheduleOnce(cb: () => void, afterTicks?: number): ScheduledHandle
  scheduleEvery(cb: () => void, everyTicks: number): ScheduledHandle
  cancel(handle: ScheduledHandle): void
}

type ScheduledHandle = number & { readonly __brand: 'ScheduledHandle' }

Error codes

type GameLoopErrorCode =
  | 'GAME_LOOP_ALREADY_RUNNING'
  | 'GAME_LOOP_NOT_RUNNING'
  | 'GAME_LOOP_PANIC'
  | 'GAME_LOOP_INVALID_STEP'

New additions for genesis

  1. Accumulator + interpolation alpha — alpha = accumulator / fixedStep passed to onRender for smooth rendering between physics steps.
  2. Panic detection — if accumulator > maxFrameTime * 3 the loop fires loop:panic event and clamps accumulator.
  3. FakeClock integration — clock option accepts FakeClock from @suaveplan/testing; tick advances deterministically in tests.
  4. OTel — game_loop.frame_time_ms histogram, game_loop.tick_total counter, game_loop.fps gauge.
  5. AbortSignal — start(signal) — loop exits cleanly when signal fires.

File layout

gaming/engine/game-loop/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/
      index.ts
    types/
      common.ts
      common.md
    core/
      game-loop/
        game-loop.ts
        game-loop.md
        game-loop.types.ts
        game-loop.test.ts
        index.ts
      tick-scheduler/
        tick-scheduler.ts
        tick-scheduler.md
        tick-scheduler.types.ts
        tick-scheduler.test.ts
        index.ts
    utils/
      frame-stats.ts
      frame-stats.md
      frame-stats.test.ts
  tests/
    integration.test.ts

Impact

Downstream: @suaveplan/ecs world.tick() is driven by this loop; @suaveplan/r3f-* rendering uses interpolation alpha. Prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events.