At the core of all games, a permanent loop is running which repeatedly gathers data from the input devices, moves all active objects in the game world and then generates the visual and audible representation of that world on the screen and on your speakers.
Time to examine this game loop then!
A Naive Game Loop
A naive implementation of a game loop could not like this:
// Continue running until we are asked to terminate
while(!this.endRequested) {
// Loop over all entities in the game, update the positions
// and render their audible and visual representations
foreach(Entity entity in this.activeEntities) {
entity.Update();
entity.Render();
}
}
}
Now we are about to encounter one of the most basic problems game programmers have had to cope with for all times: Not all PCs are equally fast, some will take longer to update the positions of your game's objects, others will not be able to render them quite as fast. Performance might even change inmidst of the game since the number of things on the screen will vary greatly.
With the above game loop, the game would simply run slower since the objects' positions will be updated less often than on a faster PC. On the other side, if the game is run on a faster machine, it might run faster than you expected and is likely to become unplayable. The next chapter will show you how to avoid that.
Performance-Independent Game Loop
In order to make a game independent of how fast the PC in runs on can run the game loop, we need to scale the amount by which objects are moved by the time the previous iteration of the game loop took. In other words, measure how much time has passed since the last time your objects were updated, calculate the up-to-date positions of all objects based on that time and render another frame.
DateTime lastIterationTime = DateTime.Now;
// Continue running until we are asked to terminate
while(!this.endRequested) {
// Temporary to avoid slight loss of time between passed time
// calculation and update of lastIterationTime.
DateTime now = DateTime.Now;
// Calculate how much time has passed since the last iteration
// and update the counter to the current time
TimeSpan timePassed = now - lastIterationTime;
lastIterationTime = now;
// Loop over all entities in the game, update the positions
// and render their audible and visual representations
foreach(Entity entity in this.activeEntities) {
entity.Update(timePassed);
entity.Render();
}
}
}
For example, consider a bullet that's moving through the game world
at a desired speed of 1 units per second:
If the game loop took 0.5 seconds, we move the bullet by 0.5 units
on each iteration of the loop. If the game loop took 1.0 seconds,
however, we move the bullet by 1 unit per iteration. In both cases,
after a second has passed, the bullet will have moved by 1 unit.
But wait! This game loop still has major problems!
Fully Performance-Independent Game Loop
Take above game loop which updates objects sequentially based on the time that has passed since the last loop iteration. Now assume you were programming a racing game and two cars were headed towards each other for a big frontal crash. Both cars are moving at 10 units per second and are 10 units apart (so after half a second, they will both have moved by 5 units towards each other, reducing their distance to zero - bang!).
Let's observe this scene with on two different computers!
John's Computer
John's computer is rather slow and is running your game at a breathtaking 1 frame per second, so each iteration of the game loop takes 1 second.
At the beginning of the update, both cars are still 10 units apart:
Now the first car's position will be updated. Since 1 second has passed from when the last iteration was performed, the car will be moved by a full 10 units:
Bang! The updated car has reached its opponent before it even had the chance to be updated also. But weren't they supposed to meet in the middle?
Rick's Computer
Rick's computer is a real performance monster. It is running your game at no less than 4 frames per second, making each iteration of the game loop take 0.25 seconds. Here, the collision will occur like this:
We'll start when the first car's position is updated. Since only 0.25 seconds have passed from when the last iteration of the game loop took place, the car will only be moved by 2.5 units:
Then the second car's position is updated, moving the car by 2.5 units as well:
Nothing happened yet. So the game will render another frame and, 0.25 seconds later, we will be in the update phase once more. Again, the first car is moved another 2.5 units:
Then the second car is also moved another 2.5 units:
Bang! The cars have hit each other in the middle.
Conclusion
Now assume John and Rick were playing a multiplayer game and continued driving to the finish line after their accident. The game would be in a different state on each computer and slowly drift even further apart.
If you're thinking that this problem could be solved with math, you're right (keyword swept collision detection). But even then, due to the different steps by which both PCs update the game world, you would still have different rounding errors on each PC, causing the game states to drift apart nevertheless.
The only solution is to advance the game in fixed time steps. You will still have rounding errors and inaccuracies like the one demonstrated above, but at least they will be the same on all computers, allowing your game to record replays, have a multiplayer mode and in-game cutscenes with predictable outcome.
Here's one method to achieve this:
public const TimeSpan StepSize = TimeSpan.FromMilliseconds(10);
public void RunGame() {
DateTime lastIterationTime = DateTime.Now;
// Continue running until we are asked to terminate
while(!this.endRequested) {
DateTime now = DateTime.Now;
// Perform as many steps as needed to catch up with the time
// passed since the last frame. If the time taken is not
// evenly dividable by the step size, this code will leave
// the remainder for the next frame
while(lastIterationTime + StepSize <= now) {
// Update the positions of all entities in the game
foreach(Entity entity in this.activeEntities)
entity.Update(timePassed);
lastIterationTime += StepSize;
}
// Loop over all entities in the game and render their
// audible and visual representations
foreach(Entity entity in this.activeEntities)
entity.Render();
}
}
The next section will discuss more reasons why we want to
seperate the update phase from the drawing phase and draw the
connection to the Game class in
Microsoft's XNA framework which provides you with a premade
game loop that you can use in your games.
Advanced Game Loops
In the previous section, we have seperated the update phase and the drawing phase in our game loop. This not only makes sense when we want to use fixed time steps, but also:
- Once your game becomes larger, you will not want to keep drawing and update all the objects in your game all the time to keep performance at reasonable levels. The subset of objects you're going to draw is different from the set of objects you'll need to move, so it makes sense to use two different loops.
- The graphics card works independently of the CPU. So if you seperate the update phase and the drawing phase, the graphics card can work rendering the current frame while the CPU is already moving the objects to their new locations for the next frame. This can increase the performance of your game by as much as 100%.
- If your game makes use of a physics engine, care has to be taken that time steps don't grow too large. Current physics engines tend to explode their simulation at large time steps. So even in a singleplayer game, seperating the update phase from the drawing phase makes sense so you can move the game objects at multiple smaller steps instead of a single big one.
All this causes our updating and drawing code to becoming much,
much more complicated once we go from pong and tetris clones to
intermediate games. Therefore it's wise to extract all this
code into seperate methods which we'll call Update()
and Draw() for now:
public void RunGame() {
DateTime lastIterationTime = DateTime.Now;
// Continue running until we are asked to terminate
while(!this.endRequested) {
DateTime now = DateTime.Now;
// Perform as many steps as needed to catch up with the time
// passed since the last frame. If the time taken is not
// evenly dividable by the step size, this code will leave
// the remainder for the next frame
while(lastIterationTime + StepSize <= now) {
Update(StepSize);
lastIterationTime += StepSize;
}
Draw();
}
}
public void Update(TimeSpan stepSize) {
// Update the positions of all entities in the game
foreach(Entity entity in this.activeEntities)
entity.Update(timePassed);
}
public void Draw() {
// Put all entities which could potentially be seen by the
// player into a list
buildPotentiallyVisibleSet();
// Render the entities in aforementioned list
foreach(Entity entity in this.potentiallyVisibleEntities)
entity.Render();
}
Game Loops in Microsoft's XNA Framework
By chance, the XNA framework provides a Game
base class that already contains a main loop just like the
one shown in this article, using the exact same names for
the Draw() and Update() methods.
This base class is contained in the
Microsoft.Xna.Framework.Game assembly and can
be used like shown in the following example:
/// <summary>Initializes the game</summary>
public MyGame() {
// Configure the main loop to advance in fixed steps of 10 ms
this.TargetElapsedTime = TimeSpan.FromMilliseconds(10);
this.IsFixedTimeStep = true;
}
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime) {
// Put all entities which could potentially be seen by the
// player into a list
buildPotentiallyVisibleSet();
// Render the entities in aforementioned list
foreach(Entity entity in this.potentiallyVisibleEntities)
entity.Render();
}
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime) {
// Update the positions of all entities in the game
foreach(Entity entity in this.activeEntities)
entity.Update(gameTime);
}
}
Now that you know how to set up a game loop, your next topic should be how to use the XNA framework to display 2D or 3D graphics on the screen!
Game Loops compared
Nice overview on how you can do things on XNA! If you want more info on different ways to implement the game loop, and compare them to each other, you can take a look at my game loop article.
Post new comment