The most intuitive way to implement movement in a game is to modify a character's position in your update loop based on their keypresses:
func Update() {
if keyboard.isDown("d") {
x += 10
}
}
In very early arcade games this is the approach that was used. On a computer with a fixed clock speed and an operating system without process switching you can get away with this approach. However, on modern computers, this approach will cause the game to run faster or slower depending on how fast the player's computer is running. This is usually a problem in most games as you want the experience to be consistent, but it's especially bad in networked multiplayer games.
delta time
Measuring the time it took to render a given frame of your game can be used to solve this issue, but it is not without drawbacks.
This time measurement is usually referred to as "delta time". If your game is running at 60fps your delta time will be 1/60th of a second. Now instead of defining your speed in terms of "pixels per frame" you can define it in terms of "pixels per second":
func Update() {
if keyboard.isDown("d") {
x += 600 * delta
}
}
I've changed the speed to be 600 pixels per second, if the initial example was expected to run at 60fps these should be equivalent.
This system works well and most games in the wild use it, but there are two major issues with using delta time.
low frame rates
In the example above, when the game is running at 60fps the character moves at a rate of 10 pixels per frame, at 30fps it's 20 pixels per frame, and at 120fps it'll be just 5 pixels per frame, but what about at 10 fps? 60 pixels in a single frame? What happens if your walls are say, 32 pixels wide and your collision detection simply checks if the new location is within a wall? Your lagging player can now skip through walls.
There are of course many different ways to address this problem, but ultimately the more complex your games physics the harder it will be to avoid situations where the physics break down as the game lags. Painfully, your game is also probably more likely to lag with more complex physics being computed every frame...
complexity
In the simply example I gave above using delta time correctly is trivial, but this is not always the case when you have non-linear physics calculations:
fixed timesteps
The other broad approach for solving these issues is to used a fixed timestep.
https://gafferongames.com/post/fix_your_timestep/
The idea is to advance your physics simulation by a fixed amount at a time. If the real time that passed between two frames is less than the fixed timestep, we simply don't advance the physics simulation at all. If it is more, we advance the physics simulation multiple times until we catch up.
Your rendering code meanwhile runs separately from your physics code which could be slower or faster than the target framerate.
You no longer need to worry about the complexity of delta time and you will avoid issues related to low frame rates, but this comes with tradeoffs... You now have a situation where your physics process could move the character 0, 1, or 2 times per display frame. This results in your character "jumping" a tiny bit when moving around.
You can solve that by interpolating between your current and previous position, but this will introduce a slight amount of lag. Generally this is worth it; especially in a 3D game.
https://bevy.org/examples/movement/physics-in-fixed-timestep/