2
\$\begingroup\$

I've made a game that has gameticks every 1/60 seconds. These gameticks take very little time to calculate (< 0.5 ms).

The game is written in Javascript and uses requestAnimationFrame (rAF). rAF fires roughly a few milliseconds (how many is determined by what the browser thinks is necessary) before (mostly) every frame output to the monitor.

The code does the following every rAF:

  • get a timestamp
  • figure out how many gameticks should have happened at that timestamp, and (if needed) run enough to catch up
  • render state after latest gametick to screen

This works fine most of the time. It runs at a consistent 60 gameticks/second, regardless of what the screen is doing (good for syncing multiplayer). It handles any framerate. The game looks smooth except, sometimes, the timestamps happen to be right around the boundary for new gameticks.

frame 10: gametick 2.01 (renders gametick 2)
frame 11: gametick 2.99 (renders gametick 2)
frame 12: gametick 4.01 (renders gametick 4)
frame 13: gametick 4.99 (renders gametick 4)
frame 14: gametick 6.01 (renders gametick 6)

When this happens, it stutters horribly. How do I make it avoid stuttering needlessly?

\$\endgroup\$
2
  • \$\begingroup\$ You might want to read previous questions on using fixed timesteps, and the interpolation techniques used to handle this "loose change" fractions of time between the state update and the rendered frame. \$\endgroup\$
    – DMGregory
    Commented Feb 21, 2018 at 18:00
  • \$\begingroup\$ I looked through some of the questions there. Most of them seem to deal with interpolation. I've done interpolation before (worked just fine), and it would indeed fix this, but I'd rather not and this seems solvable without interpolation. \$\endgroup\$
    – usernumber
    Commented Feb 21, 2018 at 19:55

1 Answer 1

1
\$\begingroup\$

What you're experiencing is called judder - it comes from a mismatch between the rate at which motion is updated and the rate at which it's displayed/sampled.

Sometimes a sample falls immediately before an update, making it seem like the object has frozen in place momentarily, sometimes it lands just after an update, making it seem like the motion has skipped ahead.

The most surefire fix for this, as mentioned in the comments, is to interpolate the displayed game state between the two most recent state updates, according to how much "loose change" time is left over between the display time and the last update time. This will generally give buttery smooth motion, and good feedback design (timing feedback to the display frame, rather than to the state update) can minimize any impact on perceived latency.

If you don't want to interpolate, then you're stuck with some amount of judder, so the best you can do is try to minimize it.

Since this is effectively a beat frequency between the two periodic processes, it gets most pronounced when the update rate and the display rate are very very similar. This maxes-out the possible error to +- 1 full frame, as shown in your example, and when we get a shortfall back-to-back with a surplus, we can jump two frames at once. And once we get into that dangerous range where display frames are falling just ahead/just shy of the update, rather than safely in the middle, we tend to stay there for a long time, since the rates are so similar.

We can combat that by shortening our game state update's fixed timestep until it's distinctly different from the display rate. This does two good things for us:

  1. Shortening the fixed timestep shrinks the maximum possible shortfall/surplus time we can have in a display frame, minimizing the displacement between where a thing should be "now" and where it's drawn.

  2. By making the rates different (relatively prime, if we can), we make it much less likely that they'll sit in problematic alignment for an extended period of time. The display frame will tend to fall more randomly within the update cycle, spreading out any judder into a frame here or there, rather than vibrating on several consecutive frames.

\$\endgroup\$
2
  • \$\begingroup\$ Is there a reason that the obvious fix of rounding the gametick to the nearest integer would work poorly? \$\endgroup\$ Commented Feb 22, 2018 at 3:58
  • \$\begingroup\$ Since judder shows up in the difference between frames, alignment to the "true" clock doesn't matter as much as alignment with the frame before. So in this way, rounding just moves the problem to the point where gametick ends in .49 / .51 instead of the point where it ends in 0.99 / 0.01. You can still get two frames back to back with two frames' of state update between them, or none, an error of +-1, even if rounding seems like it should limit the error to +-0.5. It ends up more or less the same as running the game simulation half a frame in the future/in the past. \$\endgroup\$
    – DMGregory
    Commented Feb 22, 2018 at 4:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .