Spaces:
Running
Running
File size: 2,309 Bytes
020d2dc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | // 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);
},
};
}
|