Game Loop
A game loop is a continuous loop that handles both running game logic and renders the game to the player.
Simple Game Loop
In the simplest example of a game loop, the game updates and renders at the same rate.
void GameLoop() { while (true) { Update(); // updates game logic (handle input, physics, collision, interaction, etc.) Render(); // renders game to player (images, sounds, 3d models, text, etc.) } }
This was a common way to design video games when they were first made, and came with the following pros and cons:
- Pros
- Even when the game slows down, physics and game logic are still processed in a consistent manner.
- Cons
- If either the
Update()
or theRender()
take too much time, the game will slow down. - Game may not run at full speed on weaker hardware.
- Different versions of the game must be made in order to run at different framerates.
- If either the
Timesteps
There are two main types of timesteps:
- Fixed Step - the game loop processes equal intervals of game time
- Variable Step - the game loop processes varying intervals of game time based on how much real time has passed
While simple game loops originally all used fixed timesteps, some began to use variable timesteps as a way to address known issues:
// time of last iteration through game loop DateTime lastTime; void GameLoop() { lastTime = GetRealTime(); while (true) { // calculate elapsed real time var time = GetRealTime(); var dt = time - lastTime; Update(dt); // updates game logic (handle input, physics, collision, interaction, etc.) Render(dt); // renders game to player (images, sounds, 3d models, text, etc.) lastTime = time; } }
This solved a few important issues but introduced new ones at the same time:
- Pros
- Game runs at full speed on weaker hardware.
- If either the
Update()
or theRender()
take too much time, the game keeps running at full speed. - One version of the game can run on hardware with different framerates.
- Cons
- When the game slows down
- Physics and game logic are not processed in a consistent manner.
- Player inputs may be dropped.
- When the game slows down
Independent Update and Render
Ideally game logic should be fixed step for a consistent experience, but game rendering should be able to run independently at the display's refresh rate. To achieve this Update()
must be able to run independently of Render()
, a separation many programmers are familiar with (back end / front end, model / view, etc.).
Frame Interpolation
The easiest way to implement this separation is with Frame Interpolation. The last two frames of the game state must be stored, and then Render()
simply interpolates between them based on the current time.
// fixed step processed by Update() TimeSpan fixedStep; // timestamp of 2nd most recently processed game logic tick DateTime previousTickTime; // timestamp of most recently processed game logic tick DateTime currentTickTime; // 2nd most recently processed game logic tick data object previousTickData; // most recently processed game logic tick data object currentTickData; void GameLoop() { while (true) { var time = GetRealTime(); // Update at fixed step while (time >= currentTickTime + fixedStep) { var newTickData = Update(fixedStep); // updates game logic (handle input, physics, collision, interaction, etc.) // update tick time and data previousTickTime = currentTickTime; previousTickData = currentTickData; currentTickTime += fixedStep; currentTickData = newTickData; } // calculate how far we need to interpolate towards next fixed timestep time = GetRealTime(); var interp = (time - currentTickTime).TotalSeconds / fixedStep.TotalSeconds; var interpTickData = Interpolate(previousTickData, currentTickData, interp); Render(interpTickData); // renders game to player (images, sounds, 3d models, text, etc.) } }
This is the most common approach used by commercial games today.
- Pros
- Easy to implement.
- Works at framerates both greater than tick rate and less than tick rate.
- Inputs may not be processed at tick rate when tick rate exceeds framerate.
- Cons
- Introduces render delay equal to the fixed timestep.
- Interpolation may produce an incorrect state
- For example when a player teleports from one frame to the next, the interpolated frame(s) may render the player somewhere in between the two locations, possibly inside terrain or an enemy.
- This can be fixed by implementing the ability to mark certain fields as 'no-interpolation' on certain frames, but requires a decent amount of extra work.
Partial Step
If eliminating render delay is a high priority or interpolation is unreliable, implementing the ability to process partial steps is a better solution. This requires a new UpdatePartial()
method, which is intended to handle game logic similarly to Update()
, but only creating a temporary tickData object instead of a permanent one.
// fixed step processed by Update() TimeSpan fixedStep; // timestamp of most recently processed game logic tick DateTime currentTickTime; // most recently processed game logic tick data object currentTickData; void GameLoop() { while (true) { var time = GetRealTime(); // Update at fixed step while (time >= currentTickTime + fixedStep) { var newTickData = Update(fixedStep); // updates game logic (handle input, physics, collision, interaction, etc.) // update tick time and data currentTickTime += fixedStep; currentTickData = newTickData; } // Calculate partial step size time = GetRealTime(); var interp = (time - currentTickTime).TotalSeconds / fixedStep.TotalSeconds; var partialStep = fixedStep * interp; // Update partial step var partialTickData = UpdatePartial(partialStep); // updates game logic (handle input, physics, collision, interaction, etc.) Render(partialTickData); // renders game to player (images, sounds, 3d models, text, etc.) } }
This approach still runs game logic for partial steps instead of relying on interpolation, meaning that things like teleports and collisions will still occur properly.
- Pros
- Works at framerates both greater than tick rate and less than tick rate.
- Inputs may not be processed at tick rate when tick rate exceeds framerate.
- Works at framerates both greater than tick rate and less than tick rate.
- Cons
- Harder to implement.
- Consumes more processing power since an additional partial step is calculated for each rendered frame.
- Not a good fit for CPU heavy games or games with a large game state.
- May be able to process partial steps for a small section of the game state and extrapolate the rest.
- For example process collisions for objects near the player, but ignore collisions for faraway enemies during partial steps only.
Multi Threaded
Another important improvement comes from a multi threaded game loop, in which Update()
and Render()
are run on separate threads. This prevents slowdowns on one thread from affecting the other, creating smoother and more consistent gameplay.
Be Warned! Multi threaded code is difficult to debug and can give rise to many difficult issues. Beginners are recommended to stick with a single threaded game loop, as the problems of multi threading will often outweigh its benefits.
Be careful when using async / await! They can lead to many types of bugs and errors when working on a game, and it is generally better to define all the threads/tasks you will need at startup and utilize them as needed.
Frame Interpolation
// fixed step processed by Update() TimeSpan fixedStep; // timestamp of 2nd most recently processed game logic tick DateTime previousTickTime; // timestamp of most recently processed game logic tick DateTime currentTickTime; // 2nd most recently processed game logic tick data object previousTickData; // most recently processed game logic tick data object currentTickData; // Lock object object tickDataLock = new object(); void GameLoopThread1() { while (true) { var time = GetRealTime(); // Update at fixed step while (time >= currentTickTime + fixedStep) { var newTickData = Update(fixedStep); // updates game logic (handle input, physics, collision, interaction, etc.) // update tick time and data lock (tickDataLock) { previousTickTime = currentTickTime; previousTickData = currentTickData; currentTickTime += fixedStep; currentTickData = newTickData; } } } } void GameLoopThread2() { while (true) { var time = GetRealTime(); // calculate how far we need to interpolate towards next fixed timestep var interp = 0.0; object tickData0; object tickData1; lock (tickDataLock) { interp = (time - currentTickTime).TotalSeconds / fixedStep.TotalSeconds; tickData0 = previousTickData; tickData1 = currentTickData; } var interpTickData = Interpolate(tickData0, tickData1, interp); Render(interpTickData); // renders game to player (images, sounds, 3d models, text, etc.) } }
Partial Step
// fixed step processed by Update() TimeSpan fixedStep; // timestamp of most recently processed game logic tick DateTime currentTickTime; // most recently processed game logic tick data object currentTickData; // Lock object object tickDataLock = new object(); void GameLoopThread1() { while (true) { var time = GetRealTime(); // Update at fixed step while (time >= currentTickTime + fixedStep) { var newTickData = Update(fixedStep); // updates game logic (handle input, physics, collision, interaction, etc.) // update tick time and data lock (tickDataLock) { currentTickTime += fixedStep; currentTickData = newTickData; } } } } void GameLoopThread2() { while (true) { var time = GetRealTime(); // Calculate partial step size var interp = 0.0; TimeSpan partialStep; object fixedTickData; lock (tickDataLock) { interp = (time - currentTickTime).TotalSeconds / fixedStep.TotalSeconds; partialStep = fixedStep * interp; fixedTickData = currentTickData; } // Update partial step var partialTickData = UpdatePartial(fixedTickData, partialStep); // updates game logic (handle input, physics, collision, interaction, etc.) Render(partialTickData); // renders game to player (images, sounds, 3d models, text, etc.) } }
Note that the partial update actually occurs on the render thread, not the update thread.
Notes
In practice game loops may have a lot more code. They may wait a certain amount of time between passes, do less processing on certain passes than others, contain exit logic, etc.