1
\$\begingroup\$

I was reading some game's source code on Github and saw this game loop implementation for the first time:

var lastTime = 0;
var maxTime = 1/30;

/**
 * @param {DOMHighResTimeStamp} curTime requestAnimationFrame provides this value automatically
 */
this.loop = function(curTime) {
    requestAnimationFrame(Game.loop);

    // Same as division by 1000
    var dt = (curTime - lastTime) * 0.001;

    if(dt > maxTime) {
        dt = maxTime;
    }

    for(var i = 0, len = boards.length; i < len; i++) {
        if(boards[i]) {
           boards[i].step(dt);
           boards[i].draw(Game.ctx);
        }
    }

    lastTime = curTime;
};

The key thing: The game updates and draws at every step, but the delta time is capped at the desired maximum delta. That way, if a frame takes too long, the next frame steps with a constant delta.

What are the pros and cons of this approach? I imagine it's useful for preventing huge entity movements caused by a long timestep, but can it cause problems?

Edit

Use curTime as provided by window.requestAnimationFrame in order to simplify stuff and avoid comments that don't deal with my main question.

\$\endgroup\$
  • 1
    \$\begingroup\$ possible duplicate of Tips for writing the main game loop? \$\endgroup\$ – congusbongus Sep 22 '14 at 0:59
  • 1
    \$\begingroup\$ Not sure I'm asking for tips on how to write a game loop. Just asking for feedback on a specific concrete implementation. \$\endgroup\$ – rodrigo-silveira Sep 22 '14 at 1:01
  • \$\begingroup\$ I think main reason behind this is to retain reasonable game consistency then code is stopped at breakpoints. \$\endgroup\$ – Petr Abdulin Sep 22 '14 at 4:35
  • 1
    \$\begingroup\$ +1, I think the question is very valid, because it is specific to Javascript in a browser, where you come upon different conditions than in a desktop/stand-alone environment. \$\endgroup\$ – Matthias Sep 22 '14 at 11:56
1
\$\begingroup\$

The practical reason to enforce a maximum delta time is to keep your numerical integration from breaking down. An essential principle of numerical integrators is that the smaller the time delta, the smaller the error delta. A "perfect" integrator would have a time delta of zero and would be indistinguishable from an analytical solution (neglecting floating-point error).

If you have a sudden spike where a frame takes a couple hundred milliseconds to process instead of a dozen or so, the time delta submitted to the integrator is huge and thus you get a huge spatial inconsistency. If you cap the time delta, you get a small temporal inconsistency which is harder for humans to notice.

The real solution is to separate the logic/physics processing from everything else, so you have a consistent time delta while the rendering frame rate is allowed to fluctuate and bear the brunt of whatever lag spikes come up. The fantastic article Fix Your Timestep explains how to implement this.

\$\endgroup\$
1
\$\begingroup\$

This is, in general, a bad way to accomplish this goal. Just use the callback requestAnimationFrame. Otherwise you run the risk of wasting a ton of power and draining the user's battery faster (remember laptops and mobile are more prevalent than desktops these days, so performance and power usage matters!).

The use of Date().getTime() is also wrong. This is not capable of giving you a very accurate time measurement. Always prefer performance.now() and fall back only if that's unavailable. The former will return time in milliseconds (too coarse when you're dealing with a time of 16.6ms), is not monotonic (so you can get timing glitches when the clock changes), and is not guaranteed high resolution (so it might return in chunks of 4ms or so instead of the assumed 1ms).

As to the question, yes, capping time is important, though usually I see the caps a bit higher. Otherwise any situation that pauses the app might cause a huge dt when it resumes and either break game logic or waste a huge amount of power trying to catch up.

However, it should not break game logic, as your game logic should be as completely divorced from the dt as possible. The usual link is Fix Your Timestep! The gist is to handle game logic in a fixed time step (e.g. logic always runs at 60hz) and then just use the measured dt to determine how many times you need to iterate over your game logic each frame. To get very smooth display you will need to interpolate on-screen transforms between the current state and a previous state. Overall it's more work, but very much worth it.

\$\endgroup\$
  • \$\begingroup\$ The code does use requestAnimationFrame. As for new Date().getTime() I personally would never use that in a game loop because of the constant memory allocation from new. I usually also use the DOMHighResTimeStamp from requestAnimationFrame (callback arg) instead of calling Date.now() or performance.now(), since it's already provided. \$\endgroup\$ – rodrigo-silveira Sep 22 '14 at 17:41
0
\$\begingroup\$

The major CON is inconsistency... If you do like most game programmers so and define motion in terms of distance per unit time, capping the delta per frame causes time to magically disappear, slowing down the motion and causing potentially visible artifacts when the render loop most needs to show as close to stable output as it possibly can. Changing the rate of motion of moving entities in the scene is not going to impact your frame rate AT ALL.

The second point to note is that with modern graphics hardware you will likely NEVER hit that cap unless your step method is doing far too much processing. One of our voxel terrain tech demos renders 360K verticies at over 500 fps (1/500th second per frame) on what we consider mid-level graphics hardware (AMD HD7850 GPU)... our step/update methods per object are simple and short (in fact most of the renderable object don't have step/update methods at all.. thats managed elsewhere), so the majority of the overhead in each pass through the rendering loop is pushing draw commands to the GPU...

So instead of munging something that has absolutely no effect on the problem, why not just write the code as though you are going to get sky high frame rates, then when your code fails to meet expectations, profile the code and find out what is really sucking up enough cpu time to cause the sub-normal frame rates. In other words -- don't fix the problem until there IS a problem !! :) And yes -- I'm assuming that cpu load is going to be most of the cause of any low frame rates... it takes a LOT (100s to 1000s) of itty bitty draw calls to get GPU bound during the render loop, but that is a different problem with a different solution (namely make sure your draw calls are at least doing a few thousand verts per draw call so the API is waiting for the GPU to be ready, rather than the GPU waiting for the API to send the next draw call).

\$\endgroup\$
  • \$\begingroup\$ You don't get anything like focus/resize events so you can tell when the game loop should even be running at all ?? Capping DT is a hacky way of handling the player tabbing out or whatever... but I work on desktop games where I get the full range of events about what the player is doing with the game window at any moment. I stall the game loop when a player tabs out, and reset the clock when they tab back in before restarting the loop. \$\endgroup\$ – Ascendion Sep 22 '14 at 15:13
  • \$\begingroup\$ You are making me very happy my game is far too complex to work in that sort of environment :) LOL -- Next time please consider specifying the environment you are working in so those of us with no experience in that setup can avoid answers that don't necessarily apply :) OHH and if you can, turn off locking frame rate to vertical resync.. that might get your per frame times down to something reasonable. \$\endgroup\$ – Ascendion Sep 22 '14 at 16:27

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.