2
\$\begingroup\$

I'm writing an HTML canvas game that uses requestAnimationFrame and therefore runs at 60fps, although this is more of a question about failing arithmetic than about JavaScript.

If I measure the time since the last frame in order to update my sprite positions correctly, I get a slight jitter in the movements. This is because the times are accurate to the millisecond, and 1/60s does not neatly fall on millisecond boundaries.

What I end up seeing are timings like this:

16, 17, 17, 16, 17, 17, 16, 17, 17, 16

(In reality I get more 16s than 17s because of time spent outside of the timed region of code, but you get the idea).

This timing difference means that on each frame that updates on a perfect 1/60 monitor refresh rate, I advance my sprites at an imperfect rate.

My solution is to lie about my timing. I'd like to round my timings to the nearest multiple of 1/60s, expressed in ms.

E.g. If I get around 16ms timings, I'd like to report 16. If I get around about 33ms timings (1 dropped frame), I'd like to report 33.

Is there a magical formula that will do this?

\$\endgroup\$
2
  • \$\begingroup\$ It's not really enough for an answer, but take a rolling average and then truncate to be an integer? \$\endgroup\$
    – Matt Kemp
    Feb 6, 2013 at 9:59
  • \$\begingroup\$ That could end up flattening spikes, e.g. 16,16,16,16,33,16,16... could end up truncating the 33 to a 16 by swallowing it up in the average \$\endgroup\$
    – izb
    Feb 6, 2013 at 10:02

2 Answers 2

7
\$\begingroup\$
var ms = 17 // this frame's step
var step = (1/60) * 1000 // this is your desired step
var dt = ((ms / step)|0)*step // calculate number of 'steps' in ms, truncate it to an int, multiply by step

This will fix a timestep to a multiple of 1/60. If you wanted an integer dt, just truncate again using dt|0.

I'd advise against this, though. I would either find a more accurate timing method than what you're using or fix your timestep rather than a halfway house.

\$\endgroup\$
1
  • \$\begingroup\$ +1 for that final note, although I feel bad for pushing your rep off the big round number mark. \$\endgroup\$ Feb 6, 2013 at 10:40
4
\$\begingroup\$

1) You cannot rely on any fixed rounding technique : some screens are 60Hz, some 50Hz, or even 100Hz or 120Hz (like mine), ... not to mention mobiles that might be 30Hz, 20Hz,...

2) the error of the rAF might be in fact greater than 0/1, might be 2, and if the garbage collector occurs, you might even miss a frame or two, even on a Desktop : your code has to handle this nicely.

3) For this reason and to allow to pause the game, you have to use your own game time, and not real-world time to compute the right frame in your animation :

// each rAF you do something like :
var now   = Date.now() | 0;
var delta = (now - this.previousCallTime);
if (delta < 14 ) { return };       // max rate is 66Hz.
if (delta > 60 ) { delta = 16; };  // if too many frames missed,
                                   //     consider only 1 frame elapsed
game.time   = game.time + delta ;
this.previousCallTime = now;

3) And now compute the frame with :

var frameIndex =  ( ( (game.time - this.animStartGameTime ) 
                                  / this.animFrameDuration ) | 0)  % this.FrameCount ;

4) So let us see an example with 60Hz, and 3 step 20Hz anim (=50ms) :

 frame time       16  17 17  16   17  17  17   16  17  16  
 animtation time  16  33  50  66  83  100 117  133 150   
 animation index  0   0   0   1    1   *2*   2   2   0

Ooops , the animation is choppy, we have 3 frame 0, but only 2 frame 1, then 3 frame 2 again... !!!

But you know what ? Sadly enough this is an issue that cannot be solved : you are trying to approximate a fixed integer by a sum of random integers : whatever you do there WILL be times when you do not have the regularity you seek.

the effect can be reduced if you take a higher number of frames, but only reduced.

The solution provided by Matt Kemp is not what you seek :
1. Yes, you will have regularity on the index (0,1,2,0,..,0,1,2,..) but the time each frame last will be different, so for a hero walking for instance, the player will feel like the walking speed is changing over time, which is noticable also, so you trade one glitch for another, which is far far worse with 2+ ms jitter + garbage collection occuring : the real-world case.
2. It works only in 60Hz, which is a no-go.

So if you are very sensitive to animation quality, use 8, 12, 15 frames/seconds or more, this is the only real solution.

Rq : most LCD TV do this kind of computation to handle NTSC/PAL/... 's different framerate with the same screen frequency : it is not noticeable.

I put a small code i wrote before into a JSFiddle so you can experiment and see by your self : change the parameters or the index computation function, and you will see there is no way to get around this, you can just improve by raising the number of frames.

Here : http://jsbin.com/ewowel/2/edit

Results :

"Anim same frame count (index trunced) : 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6"  
"Anim same frame count  mean : 6 error : 0"

"Anim same frame count (index trunced) : 6,6,7,6,6,6,6,6,6,6,6,6,5,6,6,6,6,6,6,7,6,6,6,5"   "Anim same frame count  mean : 6 error : 4"

"Anim same frame count (index trunced) : 7,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6"  
"Anim same frame count  mean : 6 error : 2"
\$\endgroup\$
3
  • \$\begingroup\$ With Matt Kemp's answer, surely the 3-frame-step animation can be solved.. frame t 16 17 17 16 17 17 17 16 17 anim t 16 33 50 66 83 100 117 133 150 effective t 16 32 48 64 80 96 112 128 144 anim index 0 0 0 1 1 1 2 2 2 You are effectively swallowing the error on each frame, trading a tiny amount of motion speed accuracy for smoothness. \$\endgroup\$
    – izb
    Feb 6, 2013 at 16:03
  • 1
    \$\begingroup\$ 1) the real world case is : garbage collector might occurs (even in Chrome) and make you loose a frame and 2) in Browser like Firefox, and/or slower devices the garbage collector WILL occur (use the memChaser tool to see by yourself how often), so this effect will be noticeable. But the strong reason is : you have to support any refresh rate AND the dropped frame(s) case. Test on several browsers / different devices and i am sure you will be convinced. \$\endgroup\$ Feb 6, 2013 at 18:26
  • \$\begingroup\$ The question isn't 'how do I avoid dropped frames?' The question is 'how do I best work out how many frames have been dropped?'. There will always be a precise number of dropped frames - you can't drop half a frame, but you can't get an authoritative answer to that from millisecond timings. \$\endgroup\$
    – izb
    Feb 7, 2013 at 9:32

You must log in to answer this question.

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