5
\$\begingroup\$

I've seen a lot of questions about this problem, but really no solution. Hopefully I can explain the issue well enough that I can get some sort of closure on this problem.

I'm using a Fixed Time Step based on the code found here : GAFFER ON GAMES / Fix Your Timestep! , and with that method I'm using an accumulator value to compensate for any slowdowns or speedups in the frame rate. Typically the time between frames is between 15.5 and 17.5 ms (with the fixed rate aiming for 1000/60 fps). What ends up happening is this:

enter image description here

That green chart is showing the accumulator. Which freaks out once the accumulator gets beyond 1000/60 fps. At this point you can see that because of the variance between refresh rates (15.5-17.5) sometimes the accumulator is above or below the threshold of 16.66666666... until it eventually gets to a point that the accumulator is too big to be negated by the next frame variance.

If I simply ignore the refresh rate and assume a constant 60fps the flutter never occurs, so its shouldn't be a vsync issue (right?), it's not the GC kicking in. It's an issue with this part of the code:

accumulator += frameTime;

while ( accumulator >= dt )
{
    update_logic();
    accumulator -= dt;
}

I realize the issue is when the accumulator hits ~0 it actually starts doing double updates and skipping updates for a while causing the flutter/stutter look.

It's very possible that I'm just not understanding how the accumulator is suppose to work, or perhaps the variance in screen refreshing is too much for this to work well.

Thanks in advance!

Update

Here is a screen cap of an output log showing how the variance in frameTime is causing the update call to double up and skip.

enter image description here

The items in red are when the accumulator never gets to 16.66666666...

For the time being I'm 'fixing' the issue by never letting the accumulator to get below the frameTime variance, but this doesn't feel like a real answer, just a patch.

if(accumulator < 1){
    accumulator = 1.5;
}

Perhaps this bit will help explain. If you start the accumulator at 0 and the first frame takes 15.89999ms, we don't update anything, and then if the next frame is 17.55555ms we now have (~33.44ms) so it updates twice. The more the variance in the frameTime the worse the problem.

I've noticed plenty of people with this issue, and typically the conversation ends with no posted solution, it either "goes away" or the thread dies.

\$\endgroup\$
  • \$\begingroup\$ What about problems with frameTime precision? \$\endgroup\$ – wondra Jan 5 '16 at 20:41
  • \$\begingroup\$ @wondra I believe that is the true problem. It's not reliable enough, and I get the impression that the frameTime if averaged out over a few frames is actually closer to 59.9 fps which is why slowly over 30 seconds or so the accumulator gets bigger and bigger. But how do I compensate for this variance? \$\endgroup\$ – ericjbasti Jan 5 '16 at 20:56
  • \$\begingroup\$ Show more code, it can't be that the updates are skipped 'for a while' : if you just made 2 updates sure you can skip one after maaaaayybe 2, but if it's more than 2 skipped there's an error somewhere. \$\endgroup\$ – GameAlchemist Jan 6 '16 at 12:20
  • \$\begingroup\$ @GameAlchemist you've actually described the issue. It's not a constant thing, but its very predictable (per device) which is why I think most people write it off as the GC. It's only once the accumulator gets to 0 that the issue occurs, because thats the only point > a constant of dt can flip flop if the frameTime has enough variance. These results change based on the device, with little variance you would probably never notice the issue, but with enough you see the variance at a fairly constant rate (in my case every ~20s). \$\endgroup\$ – ericjbasti Jan 6 '16 at 15:39
  • \$\begingroup\$ I've tested with the most basic of code, just a RAF that prints out the frameTime (taking out any other logic or rendering that could cause a problem) and the variance remains. \$\endgroup\$ – ericjbasti Jan 6 '16 at 15:46
2
\$\begingroup\$

The solution provided in the Blog post you mentionned is not that good, even if you follow it to its end, that is to say if you interpolate.
Because you won't be able to escape the case when the error accumulates until there's no need for update, and then obviously there will be the need for two. It's just maths here.
The interpolation hides that issue since even with the 0+2 issue, the interpolation between two steps gives you almost the +1 status, but at the cost of duplicating all positions to be able to, well, interpolate, and you might even need to store speed or other things if your game depends on it (trail effect...). Ouch.

Now you can improve the algorithm and try to minimize the mean error by considering that if you the accumulated error is 'near enough' from the logic time step, you'll will step and substract the error made. So, if say you accumulated 15 ms and your step is 16, go for an update and consider you accumulated -1.

...But... more or less you have build, with this, just a complicated version of a system doing 1 fixed step update by render frame... Still it is better if you get far from your fixed logic time step ( ex : 20Hz or 120Hz).

Now there's a way that i think is most preferable : if you use an update/logic time step significantly (at least 2 times) smaller than the render time step, then it makes sense again to use the fixed time step principle. Why small time steps could be good ? stepping is a simple process, and limit the hard-to-handle cases of tunneling and 'simultaneous' collisions, i mean collisions occurring during a single step. Still the sad thing being you can't avoid to treat them, but now you might even afford to have an ugly non optimized algorithm given their unlikeliness.

For the win i did a little fiddle, you can play with it to see the various cases you want to explore.

https://jsbin.com/vadakujeme/1/edit?js,output

You can see that with the algorithm as it stands you can be lucky, but it's very unlikely, i had to hit 'run' quite a few time to get this one : enter image description here

because most of the time, even in one second, you'll have 3, 4 frame drops :

enter image description here

Now if you use a logic step of 6, i doubt anyone will notice the 3ms average error :

enter image description here

\$\endgroup\$
  • 1
    \$\begingroup\$ I need to take some time a truly digest what you're saying here. About the blog post (that everyone swears by) the funny thing is the interpolation doesn't even hide the issue, it only hides it going in one direction (when the screen refresh rate is higher than the dt) its the silly < 16.666666 frameTimes that cause the issues. Just as a side, if I set the desired rate to ~59.97fps the issue take much much longer to occur. \$\endgroup\$ – ericjbasti Jan 6 '16 at 17:03
  • 1
    \$\begingroup\$ It's worth noting that TV in the US has a frame rate of 60000/1001 which is 59.94 FPS. See blog.abelcine.com/2009/12/23/… for example. \$\endgroup\$ – Adam Jan 7 '16 at 1:03
  • \$\begingroup\$ I'm sure I'm missing something with your answer (I apologize if thats the case). It looks like you're essentially saying always make sure your logic step is shorter than the render step, preferably by half. I get that, and I've done it and it does fix the problem for the most part. But what do I do for users with a 120hz screen, now my logic step ~ matches their refresh rate. I don't have one to know, so perhaps the stutter would happen so fast no one can see it, I don't know. \$\endgroup\$ – ericjbasti Jan 7 '16 at 14:41
  • 1
    \$\begingroup\$ Ho yes, in the high fps case, i think anyway that one should skip frames ( == lower render rate ) to 60Hz, to avoid the computer heating / the fan going crazy. I once had such a display, and Javascript isn't just efficient enough, the noise/heat/power consumption (for a laptop) is far more annoying than the very little gain in smoothness, especially for a (simple) 2D game. After all, 120Hz is for pro-gamers playing fps mainly. (if i may i posted on codepen about the animation loop : codepen.io/gamealchemist/post/animationcanvas1 ) \$\endgroup\$ – GameAlchemist Jan 7 '16 at 15:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.