4
\$\begingroup\$

Here's one way to implement delta time:

/// init ///
var duration = 5000,
    currentTime = Date.now();

// and create cube, scene, camera ect

//////

function animate() {
/// determine delta ///
    var now = Date.now(),
        deltat = now - currentTime,
        currentTime = now,
        scalar = deltat / duration,
        angle = (Math.PI * 2) * scalar;
//////

/// animate ///
    cube.rotation.y += angle;
//////

/// update ///
    requestAnimationFrame(render);
//////
}

Could someone confirm I know how it works? Here what I think is going on:

Firstly, we set duration at 5000, which how long the loop will take to complete in an ideal world.

With a computer that is slow/busy, let's say the animation loop takes twice as long as it should, so 10000:

When this happens, the scalar is set to 2.0:

scalar = deltat / duration
scalar = 10000 / 5000
scalar = 2.0

We now times all animation by twice as much:

angle = (Math.PI * 2) * scalar;
angle = (Math.PI * 2) * 2.0;
angle = (Math.PI * 4) // which is 2 rotations

When we do this, the cube rotation will appear to 'jump', but this is good because the animation remains real-time.

With a computer that is going too quickly, let's say the animation loop takes half as long as it should, so 2500:

When this happens, the scalar is set to 0.5:

scalar = deltat / duration
scalar = 2500 / 5000
scalar = 0.5

We now times all animation by a half:

angle = (Math.PI * 2) * scalar;
angle = (Math.PI * 2) * 0.5;
angle = (Math.PI * 1) // which is half a rotation

When we do this, the cube won't jump at all, and the animation remains real time, and doesn't speed up. However, would I be right in thinking this doesn't alter how hard the computer is working? I mean it still goes through the loop as fast as it can, and it still has render the whole scene, just with different smaller angles! So this a bad way to implement delta time, right?

Now let's pretend the computer is taking exactly as long as it should, so 5000: When this happens, the scalar is set to 1.0:

angle = (Math.PI * 2) * scalar;
angle = (Math.PI * 2) * 1;
angle = (Math.PI * 2) // which is 1 rotation

When we do this, everything is timsed by 1, so nothing is changed. We'd get the same result if we weren't using delta time at all!

My questions are as follows

  1. Mostly importantly, have I got the right end of the stick here?
  2. How do we know to set the duration to 5000 ? Or can it be any number?
  3. I'm a bit vague about the "computer going too quickly". Is there a way loop less often rather than reduce the animation steps? Seems like a better idea.
  4. Using this method, do all of our animations need to be timesed by the scalar? Do we have to hunt down every last one and times it?
  5. Is this the best way to implement delta time? I think not, due to the fact the computer can go nuts and all we do is divide each animation step and because we need to hunt down every step and times it by the scalar. Not a very nice DSL, as it were.
  6. So what is the best way to implement delta time?
  7. Below is one way that I do not really get but may be a better way to implement delta time. Could someone explain please?

// Globals
INV_MAX_FPS = 1 / 60;
frameDelta = 0;
clock = new THREE.Clock();

// In the animation loop (the requestAnimationFrame callback)…
frameDelta += clock.getDelta(); // API: "Get the seconds passed since the last call to this method."

while (frameDelta >= INV_MAX_FPS) {
    update(INV_MAX_FPS); // calculate physics
    frameDelta -= INV_MAX_FPS;
}

How I think this works:

Firstly we set INV_MAX_FPS to 0.01666666666 How we will use this number number does not jump out at me. We then intialize a frameDelta which stores how long the last loop took to run.

Come the first loop frameDelta is not greater than INV_MAX_FPS so the loop is not run (0 >= 0.01666666666). So nothing happens.

Now I really don't know what would cause this to happen, but let's pretend that the loop we just went through took 2 seconds to complete:

We set frameDelta to 2:

frameDelta += clock.getDelta();
frameDelta += 2.00

Now we run an animation thanks to update(0.01666666666). Again what is relevance of 0.01666666666?? And then we take away 0.01666666666 from the frameDelta:

frameDelta -= INV_MAX_FPS;
frameDelta = frameDelta - INV_MAX_FPS;
frameDelta = 2 - 0.01666666666
frameDelta = 1.98333333334

So let's go into the second loop. Let's say it took 2(? Why not 2? Or 12? I am a bit confused):

frameDelta += clock.getDelta();
frameDelta = frameDelta + clock.getDelta();
frameDelta = 1.98333333334 + 2
frameDelta = 3.98333333334

This time we enter the while loop because 3.98333333334 >= 0.01666666666

We run update

We take away 0.01666666666 from frameDelta again:

frameDelta -= INV_MAX_FPS;
frameDelta = frameDelta - INV_MAX_FPS;
frameDelta = 3.98333333334 - 0.01666666666
frameDelta = 3.96666666668

Now let's pretend the loop is super quick and runs in just 0.1 seconds and continues to do this. (Because the computer isn't busy any more). Basically, the update function will be run, and every loop we take away 0.01666666666 from the frameDelta untill the frameDelta is less than 0.01666666666.

And then nothing happens until the computer runs slowly again? Could someone shed some light please? Does the update() update the scalar or something like that and we still have to times everything by the scalar like in the first example?

\$\endgroup\$
  • 2
    \$\begingroup\$ Highly related: Fixed time step vs Variable time step \$\endgroup\$ – John McDonald May 25 '14 at 19:15
  • \$\begingroup\$ Make sure you ignore the potential for the end-user or a periodic time synchronization service to change the wall-clock. Otherwise, neither one of these solutions seems like a good idea. The first one is for sure based on the real-time clock, which can be changed at any time (and may produce backwards flowing time), and the second one is rather vague. \$\endgroup\$ – Andon M. Coleman May 25 '14 at 22:49
  • 3
    \$\begingroup\$ Date.now is no good for timing a game or animations. You really want performance.now instead. \$\endgroup\$ – Sean Middleditch May 26 '14 at 3:06
  • \$\begingroup\$ I reworked my answer slightly to try to make it more relevant to your question. Hope that helps. \$\endgroup\$ – badweasel May 27 '14 at 22:29
2
\$\begingroup\$

My answer is not specific to three.js. I'm a c and an OpenGL iOS coder. But I do think the techniques would apply.

Your estimation of what your code sample does looks right to me. But as some commenters have said, it doesn't seem like the best method to handle time in a game loop.

For example, regarding the scalar being 2.0 you said "When we do this, the cube rotation will appear to 'jump', but this is good because the animation remains real-time." This is true but also realize that there's many values in between and that it could be even higher than double. If your displayLoop is 1/60 of a second, even double is still 1/30th and most people might not even notice that much of a "jump". Much below that though and they would. But there are many steps in between. In real world your fps could fluctuate between 60 on less complex rendering and 30 on more complex renderings and in my games I think this is ok. If someone is on a slower machine it might be even less and appear much more jumpy.

But you are absolutely correct that you want to use a method that uses real time to determine how long things take to happen, and not "computer cycles".

The way I do it (in pseudo code) is:

First have these long or double floats as globals: timeThisRound, delta (timeDelta), timeLastRound. Then times with your display sync call something like this:

displayLoop() {
    timeThisRound = date();
    delta = timeThisRound - timeLastRound;
    updateElementWithDelta(delta);
    updateBasedOnTime();
    timeLastRound = timeThisRound;
    // draw elements and swap buffers
}

In iOS we have a way to sync the calling of a method or function with the display loop - which we target 60fps. But if you can't do that you could do it a different way, like attempting to call it every 0.016667 seconds. I don't know how WebGL and three.js handles the display loop.

The date() function above needs to be something that accurately returns time in seconds and fractions of a second. Then delta will also be in fractions of a second between this call of displayLoop and the last time it was called. So if your displayLoop function is being called 1/60th of a seconds, as you said, that would be 0.016667. And if something happens to make it skip a cycle or take a little longer then that number would be higher.

A couple of notes here:

  1. Don't call date() more than once in the loop or in other functions. Have timeThisRound be the master clock. This way all movement and animation will be timed to the same clock.

  2. In here I also often cap delta at .5 seconds or so to prevent huge jumps in animation. If something makes the screen update that slowly the game will slow down but I personally prefer this to animations jumping out of control. Example if a call comes in on a phone you don't want them to come back from the call with a crashed ship and "you lost" on the screen.

Back to the method - if an object is traveling 5 pixels per second you could set velocity = 5;

Then in the updateGameWithDelta() you'd do:

position = position + velocity*delta;

Since delta is in parts of a second and velocity is in pixels per second, multiplying them together will give you the amount of pixels to move for that time slice. Just like hours * miles per hour gives you miles traveled in that number of hours.

You can also do moves based on time alone, which I do a lot.

So if you have a move you want to take 2 seconds you could first set up globals:

duration=2.0;
startPosition=123.0;
endPosition=456.0;
startTime= timeThisRound + 1.0;

I code in c so put these in a data struct for the object. Then in updateBasedOnTime() you'd do:

percentThruMove = (timeThisRound - startTime) / duration;

(timeThisRound - startTime) tells you many things. First if the move hasn't started yet it will be less than 0, so the position would need to stay at the startPosition. if it's bigger than duration the move should have already ended and it should be at the ending position. Also in that case you can stop worrying about animating that object. In between the two it is a percentage through the move.

So you can clamp it to 0-1, and then set the position:

clamp(percentThroughMove, 0,1);
travelDistance = endPosition - startPosition;
currentPosition = startPosition + percentThroughMove * travelDistance;

I also usually set up a trigger to end the move checking. So instead of using a simple clamp function I make my own with ifs:

if(percentThroughMove<0.0)
    currentPos = startPos;
elseIf(percentThroughMove>1.0) {
    currentPos = endPos;
    moving = no;
}
else {
    travelDistance = endPosition - startPosition;
    currentPosition = startPosition + percentThroughMove * travelDistance;
}

The cool thing with this method is that you also have an opportunity to use a tween instead of a linear move. Like instead of *percent do *percent*percent and the move will ease in.

\$\endgroup\$

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.