Please read and seek to understand the material below. Questions and programming exercises in light yellow will be discussed in class. Please write down enough so that you will be able to participate in the discussion. If you do not understand an exercise, feel free to skip it.

A basic computer game -- really, any interactive system -- involves running code to do the following three things:

We'll tend to think of the input as coming from a computer mouse, keyboard, or gamepad, and the display being some sort of raster graphics drawn to a computer monitor.

What is a computer game you have seen or played that uses an atypical input or output device?

Thinking in C++ for a moment, this means that a Game's overall structure must look something like this:

struct Game {
	Game();
	~Game();

	//Handle input:
	void handle_input(Input i);

	//Update over time:
	void update(float elapsed_time);

	//Draw current state:
	void draw();

	/* ... */
};

What are the Game() and ~Game() functions?

What might it make sense for the game code to do in them?

The Main Loop

Now that we've got these basic functions figured out, let's talk about how the code outside the Game should go about calling them. We'll call this outside code the "main loop" both because it's the outer loop that runs the rest of the code, and because -- in a simple game -- it generally lives in main.

First Attempt: The Inferno

Given game code above, and a support library that allows reading input and computing elapsed time, we can write our first attempt at a main loop.

//library functions:

//poll_input reads input from a queue of input events:
// if the queue is empty it returns false
// otherwise it fills *input with the next event and returns true
bool poll_input(Input *input);

//get_elapsed returns the elapsed time, in seconds, since it was last called:
// NOTE: first call always returns 0.0f
float get_elapsed();
//Loop:
int main(int argc, char **argv) {
	Game game;
	//the main loop:
	while (true) {
		Input input;
		while (poll_input(&input)) {
			game.handle_input(input);
		}

		game.update(get_elapsed());

		game.draw();
	}
}

This will run our game admirably, but there's a problem: if you write games like this, anyone trying to play them on a laptop or phone is going to be very annoyed. Why?

Because the game is always running as fast as it possibly can, regardless of whether this update rate actually improves the experience.

Second Attempt: The Black Hole

To solve our inferno problem, we need to slow the game down. But how slow should it go? The classic solution to this question is "as fast as the monitor lets it." In other words, our game should only draw frames as fast as the display device it is attached to can display them.

To revise our main loop to address this problem, we'll need another library function:

//vsync_and_swap waits until swapping buffers would not cause tearing, swaps the buffers, and returns
void vsync_and_swap();

To understand this function, we'll need to take a moment to discuss framebuffers. A framebuffer is an area of memory (generally on the GPU) that holds a "frame" (or image) to be displayed. When the GPU needs to transmit image data to a monitor (generally every 60th of second, though display "refresh rates" can vary), the GPU reads data from the framebuffer and sends that data out over the cable connecting it to the monitor.

GPU showing framebuffer in memory connected to monitor
A GPU sending the contents of its framebuffer to a display, pixel-by-pixel.

Of course, it would look really weird if the GPU were to send data from a frame that wasn't finished rendering. So how can a game draw to the framebuffer while being sure that it isn't getting sent to the monitor? The key is that there is a period of time -- the "vertical blanking interval" -- during which no video is being sent to the monitor by the GPU. The start of this period of time is indicated (in old-school NTSC video) by the "vertical synchronization" pulse, or simply "vsync". In modern parlance, we use the term "vsync" to mean "wait for the GPU to indicate that the previous frame is done sending" -- regardless of whether the frame was sent as NTSC video or something a bit more modern.

pixel stream from GPU to monitor with vertical blanking period labelled
The vertical blanking period is the time between when the GPU finishes sending one frame and when it starts sending the next frame.

So now we have some idea of what the vsync part of our new library function's name means, what about the swap part? Well, back when memory was expensive, games would draw directly to the very same memory that the GPU was sending to the monitor. This "single-buffered" style of drawing was limited, because all drawing operations would need to complete inside the vertical blanking interval -- no drawing code could be running while the frame was actually being sent without resulting in visual artifacts.

The fix? More memory and a layer of indirection. (And, yes, "more memory and a layer of indirection" is a pretty common way for computer science to solve problems.) Once GPUs could afford to have storage for two framebuffers, games could do a thing called "double-buffering", where code would draw to one buffer (the "back buffer") while the GPU was showing another buffer (the "front buffer"). Then, during the vertical blanking interval, the GPU's "front buffer" and "back buffer" pointers could be swapped, and the process could repeat -- game code drawing to the (new) "back buffer", the GPU showing the (new) "front buffer", and so on.

GPU showing a front buffer being displayed while a back buffer is drawn
Modern rendering systems display from the "front" framebuffer and draw to the "back" framebuffer. When drawing is complete, the front and back pointers are swapped and the process repeats.

(By the way, swapping during the middle of a frame being sent to the monitor generally means that vertical edges in moving objects become disconnected; this artifact is colloquially known as "tearing".)

Another advantage of double buffered video output is that even if drawing code takes a really long time to finish drawing into the back buffer, the GPU can continue to send the front buffer to the monitor again and again. But this behavior can also cause problems.

When using double buffering and vertical synchronization on a 60Hz monitor, you'll probably see frame rates like 60fps, 30fps, 20fps, 15fps, ... but never a stable 45fps or 57fps or 36fps. Why is this?

By the way, the code we write in this class will all be using double-buffered graphics output. So we can safely assume that Game::draw() is drawing to the back buffer and revise our game loop as follows:

int main(int argc, char **argv) {
	Game game;
	//the main loop:
	while (true) {
		Input input;
		while (poll_input(&input)) {
			game.handle_input(input);
		}

		game.update(get_elapsed());

		game.draw();

		vsync_and_swap(); //wait until frame is shown
	}
}

It's a small change, but your friends' laptop batteries will definitely thank you.

Unfortunately, vsync is not always readily available. A confused programmer might attempt to work around the problem like this:

int main(int argc, char **argv) {
	Game game;
	//the main loop:
	while (true) {
		Input input;
		while (poll_input(&input)) {
			game.handle_input(input);
		}

		game.update(1.0f / 60.0f);

		game.draw();

		//force 60Hz frame rate:
		float frame_time = get_elapsed();
		while (frame_time < 1.0f / 60.0f) {
			frame_time += get_elapsed();
		}
	}
}

This code still runs at 100% CPU usage. Why?

Remember that comments tell you what the programmer is thinking, not what the code does. When won't this code force a 60Hz frame rate?

This approach can be made to work (at least as a battery-saver), but a system call is needed to allow the game to sleep instead of busy-waiting.

Why won't sleep work here?



C++ provides a cross-platform sleep function in the <thread> header. What is it called?

Unfortunately, our code still may suffer from a "frame rate black hole" -- so called because it involves the frame rate getting pulled into an abyss from which it will never return. This problem occurs if the game ever ends up in a situation where Game::update(t) ends up taking, say, 1.2 * t seconds to execute.

This is actually not a crazy situation for a game to find itself in -- we'll talk about one common situation that can lead to this sort of behavior in the next section. For now, just take this timing model as given and think about the consequences:

On frame 1, Game::update(0.0f) finishes in 0.0 seconds
then vsync_and_swap waits 0.01666 seconds.
On frame 2, Game::update(0.01666f) takes 0.02 seconds
then vsync_and_swap waits an additional TBD seconds.
... the rest of this table will be generated by in-page JavaScript shortly ...

That's an exponential growth in frame times.

Thankfully the fix for the frame rate black hole is surprisingly easy. Here it is:

int main(int argc, char **argv) {
	Game game;
	//the main loop:
	while (true) {
		Input input;
		while (poll_input(&input)) {
			game.handle_input(input);
		}

		//The fix is in this line:
		game.update(std::min(get_elapsed(), 1.0f / 10.0f));

		game.draw();

		vsync_and_swap();
	}
}

How does this fix work? What happens instead of exponential frame time growth?

What will the player see instead of a frame rate dropping off to zero?

Final Refinement: Ticks

Now that we have a nicely functioning main loop, I want to step inside the Game object for just a moment to discuss variable-time vs fixed-time updates.

Up until this point, we've been talking about a Game::update function that performs variable time updates. This means that it must be able to advance the game state for 0.001 seconds or 0.1 seconds (though not much more than that -- see fix above). For some tasks, like seeing if a countdown timer expired this frame, or computing the new position of an object moving at a fixed velocity, a variable time update isn't a problem; it's easy to reason about the quantities and write correct formulas. In fact, here's a chance to write a correct variable-time update yourself:

Exercise: Fast Friends

A confused programmer with a 60Hz monitor writes the following code. It works great for them, but their friend who has a fancy 240Hz gaming monitor says the game is "way too fast". Fix this update code so it works for both the programmer and their friend:

Current status:

But for other things, like friction, collision resolution, and gravity, the closed-form solutions aren't as easy to reason about. Further, sometimes you want to make sure that your game always runs exactly the same way, for example, because you need all the players in a networked game to have the same state, and that means that variable-time updates are definitely a bad idea.

The solution is to introduce a second, fixed-time update function to Game, and call this function from within update.

struct Game {
	Game();
	~Game();

	//Handle input:
	void handle_input(Input i);

	//Variable-time update:
	void update(float elapsed_time);

	//Fixed-time update helper (called by 'update'):
	void tick();
	static constexpr float Tick = 0.01f; //timestep used for tick()
	float tick_acc = 0.0f; //accumulated time toward next tick

	//Draw current state:
	void draw();

	/* ... */
	struct {
		float position = 0.0f;
		float velocity = 1.0f;
	} player;
};

void Game::update(float elapsed) {
	//handle fixed-time updates:
	tick_acc += elapsed;
	while (tick_acc > Tick) {
		tick_acc -= Tick;
		tick();
	}

	//handle variable-time updates:
	//...
}

Notice that things updated in tick and things updated in update will -- in general -- be at different points in time during the draw function.

Why is this?

Will tick-updated quantities be later or earlier in time than update-updated quantities? Why?

When could this be a problem?

Implementing a tick function like this can make it a lot easier to handle things like -- for example -- (viscous) damping, since you can just multiply by a constant. Of course, there's nothing wrong with using regular continuous-style update rules either, and defining a Tick constant make those easy to write.

void Game::tick() {
	//always called every Tick seconds, so you can write things like this and it will perform consistently:
	player.velocity *= 0.95f;

	//of course, you can always use continuous-style update formulas with the fixed "Tick" constant:
	player.position += player.velocity * Tick;
}

Wrapping Up

The main loop as described above is pretty much exactly what we'll use in the course base code to run your games.

That said, there are a few things I wanted to go over that didn't fit into the flow of the lesson above, so they are contained in this section.

Aside: Event-Driven Programs

We talk about main loops in this class because it fits the model of program execution that you are familiar with from desktop OSes: a program starts up, enters its main function, spends as much time there as it needs, and -- once main returns -- is done. But if you work in other paradigms (e.g., HTML5+JavaScript games) you may encounter a different execution model: event-driven execution.

An event-driven program doesn't have a main function. Instead, it registers callbacks with the surrounding code (e.g., the OS; the browser), and each of these callbacks executes and returns. In other words, only at specific times (during event callbacks) is the game code even running.

This may seem a bit odd at first, but realize that our main function above is basically serving as an adaptor between the desktop execution model and a simple event-driven model with three events (with our various timing revisions changing when and how the update event is fired).

So fear not! Even though you'll be writing C++ code that uses a main loop in this class, pretty much everything you learn will still work just fine in, e.g., JavaScript code with various keydown events and requestAnimationFrame callbacks.

Quitting

Notice that our Game doesn't have any facility for quitting. Modify the Game structure and the main loop as needed to support this functionality, and comment your modification explaining how it should be used. (Note: if you add additional functions, you do not need to implement them.) There are a lot of ways to do this, most of which are -- in some sense -- correct.