JavaScript Fixed Timestep Game Loop
If you are building a game with a physics engine that requires deterministic calculation, you might want to use a fixed game loop to update the physics engine.
Since we don’t have control over the JavaScript event loop, we need to understand how the event loop works to implement a good fixed game loop.
Here’s my thought process on implementing a fixed game loop in JavaScript. Let’s dive in! 😮💨
Note: Since this topic is little complicated to explain in one article, I have linked external resources to address the topic in more detail. I recommend you to read the external links to get a better understanding.
Presquites
This article assumes that you have the following knowledge. I will not go into details of these topics as they are out of the scope of this article.
- Basic knowledge of JavaScript call stack, event loop, and task queue.
- Basic knowledge of game development and physics engines.
Why fixed timestep game loop?
Most of the physics engines used in games are based on a fixed time step. This means that the physics engine update step should be consistent regardless of the frame rate.
As far as I know, game engines like Unity have a dedicated update method for the physics engine that runs at a fixed time step. This allows the physics engine to run independently from the main game loop(as known as the update loop) which runs at a variable time step depending on the frame rate.
The more variable the time step is, the more physics engine will be non-deterministic. Physics engine will produce different result depending on the frame rate. This is not good for games that need guaranteed physics calculation whether the frame rate is lagging or not. For example, games that use a physics engine and where it’s important for the game score system. Actually, most of the games that use a physics engine need deterministic physics calculation.
There’s a really good and popular article on this topic by Glenn Fiedler called Fix Your Timestep!. I highly recommend reading it.
What time step should I use?
It should be the highest fps you want to support. For example, if you want to support 144fps, the time step should be 1/144 = 6.94ms. For 60fps, the time step should be 1/60 = 16.67ms. Again, don’t use different timesteps across different refresh rate devices, as it will result in non-deterministic physics calculation.
Choose wisely! If you change the time step after the game is released, you are basically changing how the physics engine behaves.
requestAnimationFrame
?
1. Why not use requestAnimationFrame
is a browser API that runs the callback function before the next repaint. This means that the callback function will be called as soon as the browser is ready to repaint the screen. This is great for animations and rendering.
Problems
- Doesn’t guarantee a fixed time step. If the JavaScript main thread is blocked, the callback function will be delayed.
- Time step is dependent on the device’s refresh rate. The callback will be called faster on higher refresh rate devices than on lower refresh rate devices.
setTimeout
?
2. Why not use function fixedTicker(
callback: (deltaTime: number) => void,
fixedDeltaTime: number,
) {
function loop() {
callback(fixedDeltaTime);
setTimeout(loop, fixedDeltaTime);
}
loop();
}
Terrible idea! Everytime the callback function is executed, next setTimeout
will get delayed by the time it took to execute the callback function since it was registered. This will result in accumulated error over time.
Q: But what if I call
setTimeout(loop, fixedDeltaTime);
at the first line of the loop function?A: Better, but not quite. The next
setTimeout
will be queued at the right moment, but if the callback was blocking more than the fixed time step, the secondsetTimeout
execution will be delayed and the thridsetTimeout
will be delayed even more, and so on. This will result in accumulated error over time.
Also, as the HTML spec specifies, setTimeout
has a minimum delay of 4ms on nested setTimeout
calls. The browser can pick any value that’s bigger than 4 even if you set the delay to 0ms. (Usually most browsers use 4.7ms.)
This means that the time step will be at least 4ms, which is not good for high fps of more than 200fps.
setInterval
?
3. Why not use good old setInterval
is a JavaScript function that runs the callback function at a fixed interval. This seems like a perfect solution for a fixed time step, but NO it’s not.
Problems
- Doesn’t guarantee a fixed time step. Only the next callback will be queued if the current callback is running. This means if the first callback runs longer than the interval, the next callback will be called right after the first callback finishes. Ideally, we want all the subsequent callbacks to be queued in the task queue if the current callback runs longer than the interval. But it only queues the next callback. So this has similar result as
setTimeout
with callingsetTimeout
at the first line of the loop function.
How to implement fixed ticker?
I will use requestAnimationFrame
.
”Huh? You just said it’s not good for a fixed time step.”
But hear me out. This time I will implement the compensation mechanism to guarantee a fixed time step, which is the gold standard for a fixed time step.
Reason why I used requestAnimationFrame
instead of setInterval
- If the main thread is blocked, using
setInterval
doesn’t have much difference fromrequestAnimationFrame
. Both will be delayed until it’s their queue to run. requestAnimationFrame
gives a timestamp as an argument to the callback function. This is useful for calculating the delta time between frames. Of course, you can useperformance.now()
insetInterval
, but it’s just more convenient, and there is a difference in the timestamp betweenperformance.now()
and the timestamp given byrequestAnimationFrame
.requestAnimationFrame
has higher priority thansetInterval
.setInterval
will be queued in the task queue, butrequestAnimationFrame
is in a separate queue that runs as soon as the JavaScript call stack is empty and there are no microtasks remaining.- At least in Chrome, it throttles user input events when the frame is lagging behind. So it tries its best to run
requestAnimationFrame
callback as soon as possible.
Note: Most browsers pause the
requestAnimationFrame
calls when the tab is inactive to save CPU usage. This wasn’t a problem for me as my game is paused anyway when the tab is inactive. But if you want to run the physics engine even when the tab is inactive, you might want to usesetInterval
orsetTimeout
.
Delta time
Delta time in game development is the time between the two frames. Delta time can increase if the the game lags behind the target frame rate, also known as frame drop.
Real-time physics engines use an integration method to calculate the movement. And it produces different result depending on the time step.
For example, running the physics engine at 60fps with 16.67ms time step and 30fps with 33.33ms time step will result in different movement. Physics engine running at 60fps with 16.67ms time step will generate more accurate movement than 30fps with 33.33ms time step. To explain this there are many factors to consider. Instead of explaining it here, I recommend reading Gaffer On Games: Fix Your Timestep!.
performance.now()
and timestamp
given by requestAnimationFrame
Difference between performance.now()
returns the current time in milliseconds.timestamp
given byrequestAnimationFrame
is the time of last frame in milliseconds.
It’s more accurate to calculate the delta time using last frame’s timestamp than the current time. This is because the current time can be different from the last frame’s timestamp due to the time it takes to execute the callback function right after the frame is rendered. There might be some small tasks that are executed after the frame is rendered and before the callback function is executed.
Sync
While requestAnimationFrame
is synced with the browser’s repaint, setInterval
is not. This might not be a huge deal but there’s no reason to let the frame skip when you can avoid it. Also, we need to consider V-Sync. But I haven’t done any research on this topic. If you have any information, please let me know.
Scheduling: When does the callback of requestAnimationFrame get called?
If the JavaScript call stack is empty and there are no microtasks remaining, it will be called immediately. And execute any tasks until the next frame. For more a in-depth explanation, I recommend reading theses articles.
Using high resolution timestamp
Don’t forget to use a high resolution timestamp for more accurate time calculation.
Finally, the code
// Note: This is TypeScript code
class FixedUpdate {
private lastTime: number | null = null;
private accumulatedTime = 0;
constructor(
private update: (deltaTime: number) => void,
private render: () => void,
private timeStep: number,
) {
requestAnimationFrame(this.loop);
}
private loop = (currentTime: number) => {
if (!this.lastTime) {
this.lastTime = currentTime;
}
const deltaTime = currentTime - this.lastTime;
this.lastTime = currentTime;
this.accumulatedTime += deltaTime;
while (this.accumulatedTime >= this.timeStep) {
this.update(this.timeStep);
this.accumulatedTime -= this.timeStep;
}
this.render();
requestAnimationFrame(this.loop);
};
}
Note: This code is just a basic implementation of a fixed ticker. You might want to add more features like pausing the ticker, resuming the ticker, etc. Especially pause the ticker when the tab is inactive (visibilitychange event) or else the accumulated time will try to catch up all the lost time when the tab is active again.
Possible improvements
- Use a web worker to run the ticker. This takes the advantage of multi-threading. If I have time, I would definitely try this.
- Interpolate the state between the frames to make the movement smoother. This is useful when the frame rate is not the power of physics engine time step. For example, if the frame rate is 100fps and physics engine runs at 60fps.
- You would need to store the previous state such as position, rotation, etc. and interpolate between the previous state and the current state. I wanted to implement this, but didn’t have time to do it. Maybe in the future.
- But it comes with a cost of latency. You are not rendering the most up-to-date state, but the interpolated state. Maybe it’s not a big deal as it will be about half a frame behind the actual, but it’s something to consider.
Final thoughts
Delta time and a fixed timestep are a little complicated topics. It took me some time to understand the concept and implement a fixed ticker. If you are a web game developer, I hope this article helps. If you have any questions, or any suggestions, feel free to leave a comment below.
Thanks for reading! 🚀
See also
JavaScript Event Loop
- What the heck is the event loop anyway? | Philip Roberts | JSConf EU by Philip Roberts (Youtube)
- Jake Archibald on the web browser event loop, setTimeout, micro tasks, requestAnimationFrame, … by Jake Archibald (Youtube)
- Highly recommended
Fixed Timestep
- When should I use a fixed or variable time step? (StackExchange: Game Development)
- Fix Your Timestep! by Glenn Fiedler
- Highly recommended
- Deterministic lockstep by Glenn Fiedler
- Dear Game Developers, Stop Messing This Up! by Casey Muratori (Youtube)
- game-loop by Robert Nystrom