The game loop - Fish-In-A-Suit/Conquest GitHub Wiki
The game loop is the heart of your game and is responsible for updating it and rendering onto the screen. A game loop runs continuously during gameplay. Each turn of the loop, it processes user input without blocking, updates the game state, and renders the game. It tracks the passage of time to control the rate of gameplay.
It is said that the program spends nearly 90% of it's time in 10% of the code. Your game loop represents that 10%, so it has to be structured wisely so as not to cause any prooblems. The first key part that you should be familiar with is that while a game loop processes user input, it doesn't wait for it (for example, like text-editing software). It just keeps running - at it's simplest, it boils down to:
while (true) {
processInput();
update();
render();
}
processInput()
handles any user input that has happened since the last callupdate()
advances the game simulation by one stoprender()
draws the game so the player can see what happened
Each loop advances the state of the game by some amount. If we measure how quickly the game loop cycles in terms of real time, we get the game's frames per second - FPS.
There's a problem with the above game loop, however. It doesn't run at constant speed (which would later prevent accurate physics/simulation calculations). Two factors determine the frame rate. The first is the amount of work the game loop has to perform each cycle (more work = longer cycle), while the second is the speed of the underlying machine itself.
NOTE: frame rate/frames per second(FPS) is not the same as ms (milliseconds) per frame. Frame rate is the frequenc at which consecutive images called frames appear on display - in terms of the game loop, how many times the rendering method draws to the screen. Ms per frame is the amount of time it takes for one frame to be displayed onto the screen.
ms per frame = 1000ms/FPS
One simple, but an insufficient solution would be to introduce sleep time at the end of each iteration of the game loop. Say you want your game to run at 60 FPS. That gives you about 16 milliseconds per frame. As long as you can reliably do all of your game processing and rendering in less than that time, you can run at a steady frame rate. All you do is process the frame and then wait until it’s time for the next one, like so:
while (true)
{
double start = getCurrentTime();
processInput();
update();
render();
sleep(start + MS_PER_FRAME - getCurrentTime());
}
The sleep() here makes sure the game doesn’t run too fast if it processes a frame quickly. It doesn’t help if your game runs too slowly. If it takes longer than 16ms to update and render the frame, your sleep time goes negative. If we had computers that could travel back in time, lots of things would be easier, but we don’t.
Instead, the game slows down. You can work around this by doing less work each frame — cut down on the graphics and razzle dazzle or dumb down the AI. But that impacts the quality of gameplay for all users, even ones on fast machines.
Variable time step game loop
- Each update advances game time by a certain amount.
- It takes a certain amount of real time to process that.
If step two takes longer than step one, the game slows down. If it takes more than 16 ms of processing to advance game time by 16ms, it can’t possibly keep up. But if we can advance the game by more than 16ms of game time in a single step, then we can update the game less frequently and still keep up (suppose 60 FPS)
The idea then is to choose a time step to advance based on how much real time passed since the last frame. The longer the frame takes, the bigger steps the game takes. It always keeps up with real time because it will take bigger and bigger steps to get there;
double lastTime = getCurrentTime();
while (true) {
double current = getCurrentTime();
double elapsed = current - lastTime;
processInput();
update(elapsed);
render();
lastTime = current;
}
Each frame we determine how much real time passed since the last game update (elapsed
). When the game state is updated, we pass that in. The engine is then responsible for advancing the game world forward by that amount of time.
Say you’ve got a bullet shooting across the screen. With a fixed time step, in each frame, you’ll move it according to its velocity. With a variable time step, you scale that velocity by the elapsed time. As the time step gets bigger (bigger value of elapsed
), the bullet moves farther in each frame. That bullet will get across the screen in the same amount of real time whether it’s twenty small fast steps or four big slow ones. This looks like a winner:
- The game plays at a consistent rate on different hardware.
- Players with faster machines are rewarded with smoother gameplay.
However, there's a huge problem ahead:
Say we’ve got a two-player networked game and Fred has some beast of a gaming machine while George is using his grandmother’s antique PC. That aforementioned bullet is flying across both of their screens. On Fred’s machine, the game is running super fast, so each time step is tiny. We cram, like, 50 frames in the second it takes the bullet to cross the screen. Poor George’s machine can only fit in about five frames.
This means that on Fred’s machine, the physics engine updates the bullet’s position 50 times, but George’s only does it five times. Most games use floating point numbers, and those are subject to rounding error. Each time you add two floating point numbers, the answer you get back can be a bit off. Fred’s machine is doing ten times as many operations, so he’ll accumulate a bigger error than George. The same bullet will end up in different places on their machines.
This is just one nasty problem a variable time step can cause, but there are more. In order to run in real time, game physics engines are approximations of the real laws of mechanics. To keep those approximations from blowing up, damping is applied. That damping is carefully tuned to a certain time step. Vary that, and the physics gets unstable.
To infer, even the variable time step loop isn't sufficient for a decent game engine.
Fixed time step loop
One part of the engine that usually isn’t affected by a variable time step is rendering. Since the rendering engine captures an instant in time, it doesn’t care how much time advanced since the last one. It renders things wherever they happen to be right then.
We can use this fact to our advantage. We’ll update the game using a fixed time step because that makes everything simpler and more stable for physics and AI. But we’ll allow flexibility in when we render in order to free up some processor time.
It goes like this: A certain amount of real time has elapsed since the last turn of the game loop. This is how much game time we need to simulate for the game’s “now” to catch up with the player’s. We do that using a series of fixed time steps:
double previous = getCurrentTime();
double lag = 0.0;
while (true) {
double current = getCurrentTime();
double elapsed = current - previous;
previous = current;
lag += elapsed;
processInput();
while (lag >= MS_PER_UPDATE) {
update();
lag -= MS_PER_UPDATE;
}
render();
}
There’s a few pieces here. At the beginning of each frame, we update lag based on how much real time passed. This measures how far the game’s clock is behind compared to the real world. We then have an inner loop to update the game, one fixed step at a time, until it’s caught up. Once we’re caught up, we render and start over again. You can visualize it sort of like this:
Note that the time step here isn’t the visible frame rate anymore. MS_PER_UPDATE is just the granularity we use to update the game. The shorter this step is, the more processing time it takes to catch up to real time. The longer it is, the choppier the gameplay is. Ideally, you want it pretty short, often faster than 60 FPS, so that the game simulates with high fidelity on fast machines.
But be careful not to make it too short. You need to make sure the time step is greater than the time it takes to process an update(), even on the slowest hardware. Otherwise, your game simply can’t catch up.
Fortunately, we’ve bought ourselves some breathing room here. The trick is that we’ve yanked rendering out of the update loop. That frees up a bunch of CPU time. The end result is the game simulates at a constant rate using safe fixed time steps across a range of hardware. It’s just that the player’s visible window into the game gets choppier on a slower machine.