by Izak Halseide on 2023.04.08 (last updated on 2023.04.08)
This is a short article about what a game developer should know about the computer programs that run games. This is not about gameplay loops, which are loops that game players themselves are put into. This is a simple concept that I think is taken for granted in game programming tutorials and needs a more explicit explanation. I think it is necessary to explicitly state the mechanics of using a game loop in an architecture. The page at GameProgrammingPatterns.com and this page from GafferOnGames.com are excellent reads. I am assuming that the game in question is a real-time graphical game that responds to user input immediately, as opposed to a command line interface game that has buffered input.
In a game, you write the code for how the game world behaves over time, which expands into how each type of game object behaves over time. More specifically, you break down time into slices, time steps. You program what each object does at each game update call or frame or tick or time step. This constraint comes from the fact that you need to display the game world on the screen at a certain frame rate so that things appear to be moving. Since the output needs to sliced up into a sequence of frames, like a movie, you have no choice but to somehow calculate what the game world is like in those different frames. Also, it is difficult to model the behaviors of many interesting game objects as a long-term function of time, but it is simpler to describe what a game object does next in a short period of time at each step.
How do computer games work with these time slices? Well, there is one essential idea for game programs. Games can run for a long time or an indefinitely long time. In programming, running the game for a long time requires a loop of some kind. Usually this loop is infinite or only broken out of when the player closes and quits the game. The time steps are then simulated in this loop as long as the game is running.
So, somewhere in every game program is a loop that in general means the same thing as the C code below (ignoring the time step for now):
while (!game_should_quit) { update_game(); }
The elements game_should_quit
and update_game
would be substituted with the proper variables or function calls for your
game architecture.
This loop is fine and dandy, but there can be a big problem with just updating the game like this without a time step duration variable. Typically you are writing a game that will run on multiple different computers, which will simulate and run the game at different speeds. The performance of the computer's hardware that the game will run on affects the speed of the game. If you adjust speeds solely to work on the computer you are developing on, then a different player who uses a slightly faster or slower computer will run the game, and it will feel completely different or could be unplayable due to being too fast or too slow.
A common solution to this is to introduce a time step variable or parameter, usually called "delta time", "delta t", or just "dt". This could look like the following in C code:
while (!is_quit) { float dt = time_since_last_update(); update_game(dt); }
While there is some nuance about whether you want a fixed time step or a variable time step, you usually want the computer that the game is running on to determine the time step amount before the loop begins or continuously during each loop tick, like in the example above. This can make the game physics/simulation updates not run faster or slower based on the computer's performance. A more performant computer will have a smaller time step than a less performant computer, and the more performant computer will execute more of these smaller time steps and thus have a higher FPS but will still get the same (or at least close enough) result in the physics updates. Having a smaller time step can lead to more accurate results, but if your game physics are mostly linear calculations, there should not be much of a difference between having a few big time steps versus many smaller time steps.
So how is the time step dt
used in the game update function
update_game
? Generally, you use it as a multiplier for physics
steps. Say that you want a bullet to move at 50 pixels in a second. If you
were running at 1 frame per second, the code would then look like this:
bullet_x_pixels += 50.0f;
However, you want way more than 1 FPS. Say that you ideally want 60 FPS.
Then your game_update
function runs 60 times in a second. Each
update tick, you move the bullet by how fast it moves in a frame. Ideally
you may want 60 frames per second, so your formula for speed would be
something like the following:
bullet_x_pixels += 50.0f / 60.0f;
The above code will run 60 times in a second, and by the time one second has passed, the bullet will have moved: (50.0 / 60.0) * 60 pixels, which is 50 pixels. The equivalent math is replacing the division by 60 with a multiplication by 1/60:
bullet_x_pixels += 50.0f * (1/60.0f);
Then, if we extract the 1/60 into a variable called dt
, we get
the following:
float dt = 1/60.0f; bullet_x_pixels += 50.0f * dt;
Then, the last step would to let dt
be a parameter to the
function that the bullet update code resides in. This gets us a general form
that we can apply in any update code. So, in general, we can specify what we
want to happen in our game per second and then multiply by the
fraction of a second that each game tick (a function call to
update_game
) represents. The dt
value is passed as
a parameter to many other functions that update_game
may call.
So, much of your game update logic ends up being a function of
dt
, rather than time, t
(which starts to look a
lot like function
integration)!
A typical strategy is to have a hierarchical function call tree to update your game:
∟ main_loop() ∟ update_game() ∟ update_gui() ∟ update_world() ∟ update_particles() ∟ update_entities() ∟ update_players() ∟ update_a_player() ∟ update_bullets() ∟ update_a_bullet() ∟ update_enemies() ∟ update_a_zombie() ∟ update_a_fireball()
And then dt
is passed to each function, or it is a global
variable, or a larger data structure of game info, including
dt
, is passed to every function or is globally accessible. Of
course, if you must pass dt
as a function parameter to
update_a_fireball
, for example, then you will probably end up
having to pass it down from update_enemies
, which gets it
passed from update_entities
, which gets it passed from
update_world
, which gets it passed from
update_game
.
t
or dt
However, your game logic might not all depend on dt
, or it
could be better to have some logic depend on elapsed game time,
t
. A function of time is more numerically stable because
floating point errors do not accumulate on a per-frame basis. Also,
behaviors that are only a function of t
instead of
dt
do not need to store as much current state from the last
frame. An amazing example that exploits this fact, in a particle system, can
be seen in this YouTube video about
running particle systems efficiently on the PlayStation. Another example is
animation. If you have a sphere that shrinks and grows at a certain rate over
time, you could express it in terms of dt
or t
. If
you express it in terms of dt
, then you specify how much it
changes over a little slice of time and add that to the sphere's radius,
which needs to be stored as state somehow. But, if you express it in terms
of t
, then you always just compute the sphere's radius to be
some function of time that does not require knowledge of the sphere's
previous state, the radius.
(I may make a web page of its own about functions of dt
versus
functions of t
in games, because it seems that a bunch of
people mistakenly think that they should always or only
use dt
.)
Depending on your game's technology stack (the game, engine, libraries, operating system), you may or may not have control of the event loop. In pygame, for example, you do have main control, but you need to query the event queue to make sure the main window keeps functioning. Other setups may require you to provide a callback function for game updates that gets called at times determined by the OS or the library, and so game loop is outside of your control.
Below is an empty example main game loop for use in pygame:
import pygame ORIGINAL_WIDTH = 1280 ORIGINAL_HEIGHT = 720 TARGET_FPS = 60 pygame.init() screen = pygame.display.set_mode((ORIGINAL_WIDTH, ORIGINAL_HEIGHT)) def update_game(dt): # add the start of your game logic here pass clock = pygame.time.Clock() should_quit = False # This try-finally block makes sure that pygame quits when Python has an error try: while not should_quit: if pygame.event.get(pygame.QUIT): should_quit = True dt = clock.tick(TARGET_FPS) update_game(dt) pygame.display.flip() finally: pygame.quit()