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
- Accumulator + interpolation alpha —
alpha = accumulator / fixedSteppassed toonRenderfor smooth rendering between physics steps. - Panic detection — if
accumulator > maxFrameTime * 3the loop firesloop:panicevent and clamps accumulator. - FakeClock integration —
clockoption acceptsFakeClockfrom@suaveplan/testing; tick advances deterministically in tests. - OTel —
game_loop.frame_time_mshistogram,game_loop.tick_totalcounter,game_loop.fpsgauge. - 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.