Hey everyone, I’m working on a small canvas pixel game, and I update the sprite frame from elapsed time so movement stays smooth. It mostly works, but if the tab stalls for a moment, the animation can jump 2 or 3 frames and the walk cycle looks broken.
let last = performance.now();
let frame = 0;
let acc = 0;
const frameMs = 100;
function loop(now) {
acc += now - last;
last = now;
if (acc >= frameMs) {
frame = (frame + Math.floor(acc / frameMs)) % 6;
acc %= frameMs;
}
draw(frame);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
What is the usual fix here if I want timing to stay honest but I also do not want sprite frames to visibly skip after a lag spike?
@BobaMilk yup, it’s skipping because Math.floor(acc / frameMs) fast-forwards the walk cycle after a hitch. If the tab pauses for 300ms, you add 3 frames in one paint, which is honest to elapsed time but looks bad for a sprite loop.
The usual fix is to separate gameplay time from visual animation time. Let movement/physics use real dt, but cap the animation side so it only advances one pose per render, or clamp the animation dt before it hits the sprite accumulator.
js
let last = performance.now();
let frame = 0;
let animAcc = 0;
const frameMs = 100;
const maxAnimDt = 100; // don't let a hitch dump multiple frames into animation
function loop(now) {
const dt = now - last;
last = now;
// use full dt for gameplay if you want timing to stay honest
// updateMovement(dt);
// but soften hitches for visual animation
animAcc += Math.min(dt, maxAnimDt);
if (animAcc >= frameMs) {
frame = (frame + 1) % 6; // at most one visible pose change this render
animAcc -= frameMs; // keep leftover time instead of zeroing it out
}
draw(frame);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
Small but important detail: use animAcc -= frameMs, not animAcc %= frameMs, if you’re intentionally limiting animation to one step. %= would throw away the extra whole-frame time and make the animation drift more than you probably want.