// Fixed-timestep game loop with render interpolation. // // Logic advances in exact 1/60s steps so behaviour is frame-rate independent and // reproducible. Rendering happens once per animation frame and interpolates between // the previous and current logic states using `alpha`, so motion stays smooth even // when the display refresh and the logic step do not line up. export function createLoop({ step = 1 / 60, update, render, maxFrameTime = 0.25, maxSteps = 8 }) { let accumulator = 0; let previousTime = 0; let rafId = 0; let running = false; // Debug readout. Owned here because this is the only place that sees real frame times. const stats = { fps: 0, steps: 0, frames: 0, panics: 0 }; let fpsElapsed = 0; let fpsFrames = 0; function tick(now) { // A frame can already be queued when stop() runs, so cancelling the handle // is not enough on its own — check the flag before doing any work or // scheduling the next one. if (!running) return; rafId = requestAnimationFrame(tick); let frameTime = (now - previousTime) / 1000; previousTime = now; if (!Number.isFinite(frameTime) || frameTime < 0) frameTime = 0; // A tab that was backgrounded returns a huge delta. Clamp it rather than // simulating thousands of steps at once. if (frameTime > maxFrameTime) frameTime = maxFrameTime; accumulator += frameTime; let steps = 0; while (accumulator >= step && steps < maxSteps) { update(step); accumulator -= step; steps++; stats.steps++; } // If we hit the step ceiling we are running slower than real time. Drop the // backlog instead of spiralling. if (steps === maxSteps && accumulator >= step) { accumulator = 0; stats.panics++; } render(accumulator / step, frameTime); stats.frames++; fpsElapsed += frameTime; fpsFrames++; if (fpsElapsed >= 0.5) { stats.fps = Math.round(fpsFrames / fpsElapsed); fpsElapsed = 0; fpsFrames = 0; } } return { stats, start() { if (running) return; running = true; previousTime = performance.now(); accumulator = 0; rafId = requestAnimationFrame(tick); }, stop() { running = false; cancelAnimationFrame(rafId); }, }; }