astara / src /loop.js
cloudronin's picture
Torch demo: silhouette world, torch reveal, sword rule
020d2dc verified
Raw History Blame Contribute Delete
2.31 kB
// 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);
},
};
}