Programming with Time
As game developers, one of the big differences between our work and traditional application development is that in everything we do we need to be mindful of time. The amount of time that has elapsed since something has happened, the amount of time it takes to perform an action, or determining how much progress has been made based on ratios of current elapsed time vs total expected time.
The Incredible Machine
Back in 1993, an amazing game was developed called The Incredible Machine. This game was a physics based puzzle game where you build complex contraptions that try to accomplish some small goal, such as getting a ball in a basket, turning on a light switch, or frightening a cat. You used various pieces to assemble a solution, hit play, and watched your contraption run. A few years ago I installed this game on a modern computer and built a contraption to solve the first level. However, when I hit play, the game ran at a lightening speed. It was so quick, I couldn’t even see what happened!
Need for Speed: Rivals
Need for Speed: Rivals, released in 2013, chose to target 30 frames per second across all platforms (PS3, PS4, Xbox, Xbox One, PC). During his review of the game, TotalBiscuit unlocked the PC version of the game to run at 60 fps. Let’s take a look at what happened.
(Relevant content between 5:40 - 6:30):
https://www.youtube.com/watch?v=eDA37BmvNwM&t=5m40s
What’s the deal? Why do old games run quick?
Games rely on a feedback loop. The game displays something on the screen, a player provides input based on that display, the game processes it, repeat.
This loop is also referred to as the “main game loop”, and you frequently see it in code as:
while (!quit) {
processInput();
update();
render();
}
Games, and other real-time applications, execute this loop over and over again. This loop is the very nature of interactivity between a computer simulation and a human.
Let’s take a look at that update function in a bit more detail. Say we have one object in our game, and while the game is running we want to move its position across the screen. Our update function may look something like:
update() {
position += movementDelta;
}
Every time update is called, it changes the position of the object by some amount (called movementDelta
here). Now ask yourself, how quickly will the main game loop execute? How many times will my game update every second? How fast will this object move across the screen? The answer is - it depends!
Controlling how fast we update
With the main game loop presented above, the loop with execute as fast as possible. On my really slow computer, the main game loop will take longer to execute, the update function will be called less frequently, and the object in my game will move slower. On your high-end gaming PC, it’ll execute the main game loop much faster than on my computer, and your object will end up moving much faster. We need to fix this!
Fixed Timesteps
Timesteps, delta time, dt, Δt, and tick rate are all phrases used to describe how quickly the main game loop is running, and how much time has elapsed each time it executes.
We need the object in our game to move at the same rate, no matter what computer the game is running on. Let’s start with the easiest approach: call the update the same number of times no matter how fast the main game loop is running.
while (true) {
startTime = getCurrentTime();
processInput();
update();
render();
wait(startTime+FIXED_DT-getCurrentTime());
}
The main game loop above is the same as our original one, with one minor change: The last statement in the loop tells the game to wait until some time has elapsed before executing the loop again. Using this strategy, we can set the FIXED_DT
variable to some value, and this line of code will cause the computer to wait until that amount of time has passed. If we were to set FIXED_DT
to 0.02, this would cause the main game loop to run 50 times per second, regardless of the hardware. Your high-end computer will just spend more time waiting on this one line of code than my slower computer. Great! Now the update function will be called at the same rate on both of our computers, and the object in our game will move at the same rate!
…but wait a second.
What happens when the game runs slow? Like, really slow. Like, my antivirus software just decided to run a scan, and the 4k video I’m watching is decompressing more frame data, and I just threw a grenade into the pile of boxes I spent the last hour building to see how well the physics works in this game. Well, that means your main game loop will run slower, meaning your update function won’t be called 50 times per second anymore, so your game will feel like it’s suddenly running in slow motion. Hm.
Variable Timesteps
What if instead of reducing how often we call the update function, we gave it additional knowledge on how fast our game is running. Essentially, we don’t limit how fast the main game loop runs, but we tell the update function how much time has passed.
while (true) {
currentTime = getCurrentTime();
deltaTime = currentTime - lastTime;
processInput();
update(deltaTime);
render();
lastTime = currentTime;
}
In this main game loop, notice how the update
function has a new parameter: deltaTime
. This deltaTime is calculated based on how much real world time has elapsed since the last time the update function was called. Providing this to our update function allows the logic to adjust accordingly. Here’s a new version of the update function, which takes advantage of this knowledge:
update(deltaTime) {
position += movementDelta * deltaTime;
}
Notice how far the object moves is now based on how much time has elapsed. As some concrete examples: Let’s say in our game, we have movementDelta
set to 10, meaning we’d like our object to move 10 units per second. On my slow computer, which only updates only twice per second, the deltaTime
would be 0.5. That means, every half-second update, my object moves 5 units. On your fast computer, you update 10 times per second. That would make deltaTime
be 0.1. When your update function runs, it’ll change the position of the object by 1 unit. When we look at our computers side-by-side, yours will look smoother because it updates the object 10 times by 1 unit, mine will look jumpy because it updates the object 2 times by 5 units, but both of our objects will move at the same rate.
For a visual, here are three objects, all moving at the same speed across the screen. The top one represents my slower computer, updating the object less frequently but moving it a further distance with each update. The bottom one represents your high-end computer, updating the object more frequently but moving it by smaller increments. This is all driven just by taking deltaTime
into account:
How do I use this knowledge?
Going forward, always think about your update functions running at different frequencies on different machines. Use this to consider where and how you take deltaTime
into account.
One example, to move a objects at a set rate:
update(deltaTime) {
position += movementSpeed * deltaTime;
}
Another example, logic that should only happen at a given interval, use an accumulator (fancy name for a variable that counts up):
update(deltaTime) {
accumulator += deltaTime;
if (accumulator > event_frequency) {
// Do event logic
accumulator -= event_frequency;
}
}
What about other systems, like Physics?
Physics systems typically need to be updated at a fixed rate. To do this, they commonly use the accumulator method mentioned above. Refer to this article for more information about integrating a physics system to a variable rate game:
Fix Your Timestep! | Gaffer On Games
When writing gameplay code, we frequently want to perform actions that only happen once every time the physics updates, such as applying forces. We certainly don’t want to apply additional forces every time an update function is called, and instead want to do that sort of logic only once as the underlying physics system is updated.
Additional Reading
Analysis of four different ways to structure your main game loop: