We might as well start this series off with the core game loop. It's an interesting problem that's a lot more complicated than it seems on the surface. The method I'm going to cover is shamelessly borrowed from deWiTTERS, and I've published my Rust implementation.
Different hardware is going to perform differently. A faster CPU means a game can be updated more often every second, and a good GPU could render it at thousands of frames per second (if we were to ignore VSync).
But a key part of gamedev is a consistent experience; we don't want the player's hardware to determine the speed at which the game world ticks along. The simulation should be identical in all environments, and the only differing factor between players should be their frame rate or quality of graphics.
This is where a fixed timestep comes in. This simply means the timestep at which the game world is updated is fixed at a predefined rate, for example 20 ticks per second (tps).
The chosen rate should be high enough that the game is still playable and responsive - there are only 50ms between each tick at 20tps so there wouldn't be noticeable lag between each update, e.g. NPCs wouldn't appear obviously slow-witted with such a reaction delay. It should also be low enough that most hardware can comfortably keep it up, fitting each tick into its allotted time slice.
Frantic frame rate
Our game is now merrily ticking along at the fixed rate of 20 times per second, no matter how much CPU you throw at it. But rendering it at 20 frames per second (fps) is a recipe for a stuttery, worse-than-a-movie lagfest.
Rendering at 60fps at a minimum would be ideal. But if we just take our 20tps simulation and jam it on the screen 3 times as fast, we have the same stammering slide show as before, this time paying more for the privilege.
What we should do instead is smooth the game state over time, giving the illusion of a 60tps simulation. This can be done through simple interpolation of game states1.
Each time a frame is rendered (every ~16ms for 60fps), this falls on an instant anywhere between the previous tick and the next. Treating that ratio as a floating point number between
1.0 allows for easy interpolation, where
0.0 is the previous state,
1.0 the current, and everything else in-between.
It's also possible to extrapolate instead, predicting the future game state from the current, e.g.
extrapolated_pos = current_pos + (current_velocity * interpolation). This is dangerous and more error-prone however; if the prediction is wrong and didn't account for collision resolution for example, the next frame might correct it suddenly and lead to jerky motion. Interpolating between 2 knowns is easier and safer.
In my unnamed game engine, interpolation is currently only needed for moving objects, which includes entities and the camera. It's a simple case of keeping track of the last known position, and lerping between that and the current position.
It's important to note that this interpolation is done right on the boundary between the simulation and the renderer. All game logic works solely with the true position, and only when it comes to passing the position of an entity to the renderer do we do this interpolation.
I've extracted this simple yet fiddly functionality out of the game engine into a small, separate crate. It is engine agnostic, and will hopefully help someone save a bit of time in the future.
In this context, 'game state' refers to the parts of the simulation that the player wants to see update in real-time, e.g. positions of moving entities, animation frames. ↩