The Game Loop and Game Class
One of the big differences between a game and any other program is that a game must update many times per second for as long as the program runs. A game loop is a loop that controls the overall flow for the entire game program. Like any other loop, a game loop has code it executes on every iteration, and it has a loop condition. For a game loop, you want to continue looping as long as the player hasn’t quit the game program.
Each iteration of a game loop is a frame. If a game runs at 60 frames per second (FPS), this means the game loop completes 60 iterations every second. Many real-time games run at 30 or 60 FPS. By running this many iterations per second, the game gives the illusion of continuous motion even though it’s only updating at periodic intervals. The term frame rate is interchangeable with FPS; a frame rate of 60 means the same thing as 60 FPS.
Anatomy of a Frame
At a high level, a game performs the following steps on each frame:
It processes any inputs.
It updates the game world.
It generates any outputs.
Each of these three steps has more depth than may be apparent at first glance. For instance, processing inputs (step 1) clearly implies detecting any inputs from devices such as a keyboard, mouse, or controller. But these might not be the only inputs for a game. Consider a game that supports an online multiplayer mode. In this case, the game receives data over the Internet as an input. In certain types of mobile games, another input might be what’s visible to the camera, or perhaps GPS information. Ultimately, the inputs to a game depend on both the type of game and the platform it runs on.
Updating a game world (step 2) means going through every object in the game world and updating it as needed. This could be hundreds or even thousands of objects, including characters in the game world, parts of the user interface, and other objects that affect the game—even if they are not visible.
For step 3, generating any outputs, the most apparent output is the graphics. But there are other outputs, such as audio (including sound effects, music, and dialogue). As another example, most console games have force feedback effects, such as the controller shaking when something exciting happens in the game. And for an online multiplayer game, an additional output would be data sent to the other players over the Internet.
Consider how this style of game loop might apply to a simplified version of the classic Namco arcade game Pac-Man. For this simplified version of the game, assume that the game immediately begins with Pac-Man in a maze. The game program continues running until Pac-Man either completes the maze or dies. In this case, the “process inputs” phase of the game loop need only read in the joystick input.
The “update game world” phase of the loop updates Pac-Man based on this joystick input and then also updates the four ghosts, pellets, and the user interface. Part of this update code must determine whether Pac-Man runs into any ghosts. Pac-Man can also eat any pellets or fruits he moves over, so the update portion of the loop also needs to check for this. Because the ghosts are fully AI controlled, they also must update their logic. Finally, based on what Pac-Man is doing, the UI may need to update what data it displays.
The only outputs in the “generate outputs” phase of the classic Pac-Man game are the audio and video. Listing 1.1 provides pseudocode showing what the game loop for this simplified version of Pac-Man might look like.
Listing 1.1 Pac-Man Game Loop Pseudocode
void Game::RunLoop() { while (!mShouldQuit) { // Process Inputs JoystickData j = GetJoystickData(); // Update Game World UpdatePlayerPosition(j); for (Ghost& g : mGhost) { if (g.Collides(player)) { // Handle Pac-Man colliding with a ghost } else { g.Update(); } } // Handle Pac-Man eating pellets // ... // Generate Outputs RenderGraphics(); RenderAudio(); } }
Implementing a Skeleton Game Class
You are now ready to use your basic knowledge of the game loop to create a Game class that contains code to initialize and shut down the game as well as run the game loop. If you are rusty in C++, you might want to first review the content in Appendix A, “Intermediate C++ Review,” as the remainder of this book assumes familiarity with C++. In addition, it may be helpful to keep this chapter’s completed source code handy while reading along, as doing so will help you understand how all the pieces fit together.
Listing 1.2 shows the declaration of the Game class in the Game.h header file. Because this declaration references an SDL_Window pointer, you need to also include the main SDL header file SDL/SDL.h. (If you wanted to avoid including this here, you could use a forward declaration.) Many of the member function names are self-explanatory; for example, the Initialize function initializes the Game class, the Shutdown function shuts down the game, and the RunLoop function runs the game loop. Finally, ProcessInput, UpdateGame, and GenerateOutput correspond to the three steps of the game loop.
Currently, the only member variables are a pointer to the window (which you’ll create in the Initialize function) and a bool that signifies whether the game should continue running the game loop.
Listing 1.2 Game Declaration
class Game { public: Game(); // Initialize the game bool Initialize(); // Runs the game loop until the game is over void RunLoop(); // Shutdown the game void Shutdown(); private: // Helper functions for the game loop void ProcessInput(); void UpdateGame(); void GenerateOutput(); // Window created by SDL SDL_Window* mWindow; // Game should continue to run bool mIsRunning; };
With this declaration in place, you can start implementing the member functions in Game.cpp. The constructor simply initializes mWindow to nullptr and mIsRunning to true.
Game::Initialize
The Initialize function returns true if initialization succeeds and false otherwise. You need to initialize the SDL library with the SDL_Init function. This function takes in a single parameter, a bitwise-OR of all subsystems to initialize. For now, you only need to initialize the video subsystem, which you do as follows:
int sdlResult = SDL_Init(SDL_INIT_VIDEO);
Note that SDL_Init returns an integer. If this integer is nonzero, it means the initialization failed. In this case, Game::Initialize should return false because without SDL, the game cannot continue:
if (sdlResult != 0) { SDL_Log("Unable to initialize SDL: %s", SDL_GetError()); return false; }
Using the SDL_Log function is a simple way to output messages to the console in SDL. It uses the same syntax as the C printf function, so it supports outputting variables to printf specifiers such as %s for a C-style string and %d for an integer. The SDL_GetError function returns an error message as a C-style string, which is why it’s passed in as the %s parameter in this code.
SDL contains several different subsystems that you can initialize with SDL_Init. Table 1.1 shows the most commonly used subsystems; for the full list, consult the SDL API reference at https://wiki.libsdl.org.
Table 1.1 SDL Subsystem Flags of Note
Flag |
Subsystem |
SDL_INIT_AUDIO |
Audio device management, playback, and recording |
SDL_INIT_VIDEO |
Video subsystem for creating a window, interfacing with OpenGL, and 2D graphics |
SDL_INIT_HAPTIC |
Force feedback subsystem |
SDL_INIT_GAMECONTROLLER |
Subsystem for supporting controller input devices |
If SDL initializes successfully, the next step is to create a window with the SDL_CreateWindow function. This is just like the window that any other Windows or macOS program uses. The SDL_CreateWindow function takes in several parameters: the title of the window, the x/y coordinates of the top-left corner, the width/height of the window, and optionally any window creation flags:
mWindow = SDL_CreateWindow( "Game Programming in C++ (Chapter 1)", // Window title 100, // Top left x-coordinate of window 100, // Top left y-coordinate of window 1024, // Width of window 768, // Height of window 0 // Flags (0 for no flags set) );
As with the SDL_Init call, you should verify that SDL_CreateWindow succeeded. In the event of failure, mWindow will be nullptr, so add this check:
if (!mWindow) { SDL_Log("Failed to create window: %s", SDL_GetError()); return false; }
As with the initialization flags, there are several possible window creation flags, as shown in Table 1.2. As before, you can use a bitwise-OR to pass in multiple flags. Although many commercial games use full-screen mode, it’s faster to debug code if the game runs in windowed mode, which is why this book shies away from full screen.
Table 1.2 Window Creation Flags of Note
Flag |
Result |
SDL_WINDOW_FULLSCREEN |
Use full-screen mode |
SDL_WINDOW_FULLSCREEN_DESKTOP |
Use full-screen mode at the current desktop resolution (and ignore width/height parameters to SDL_CreateWindow) |
SDL_WINDOW_OPENGL |
Add support for the OpenGL graphics library |
SDL_WINDOW_RESIZABLE |
Allow the user to resize the window |
If SDL initialization and window creation succeeds, Game::Initialize returns true.
Game::Shutdown
The Shutdown function does the opposite of Initialize. It first destroys the SDL_Window with SDL_DestroyWindow and then closes SDL with SDL_Quit:
void Game::Shutdown() { SDL_DestroyWindow(mWindow); SDL_Quit(); }
Game::RunLoop
The RunLoop function keeps running iterations of the game loop until mIsRunning becomes false, at which point the function returns. Because you have the three helper functions for each phase of the game loop, RunLoop simply calls these helper functions inside the loop:
void Game::RunLoop() { while (mIsRunning) { ProcessInput(); UpdateGame(); GenerateOutput(); } }
For now, you won’t implement these three helper functions, which means that once in the loop, the game won’t do anything just yet. You’ll continue to build on this Game class and implement these helper functions throughout the remainder of the chapter.
Main Function
Although the Game class is a handy encapsulation of the game’s behavior, the entry point of any C++ program is the main function. You must implement a main function (in Main.cpp) as shown in Listing 1.3.
Listing 1.3 main Implementation
int main(int argc, char** argv) { Game game; bool success = game.Initialize(); if (success) { game.RunLoop(); } game.Shutdown(); return 0; }
This implementation of main first constructs an instance of the Game class. It then calls Initialize, which returns true if the game successfully initializes, and false otherwise. If the game initializes, you then enter the game loop with the call to RunLoop. Finally, once the loop ends, you call Shutdown on the game.
With this code in place, you can now run the game project. When you do, you see a blank window, as shown in Figure 1.1 (though on macOS, this window may appear black instead of white). Of course, there’s a problem: The game never ends! Because no code changes the mIsRunning member variable, the game loop never ends, and the RunLoop function never returns. Naturally, the next step is to fix this problem by allowing the player to quit the game.
Figure 1.1 Creating a blank window
Basic Input Processing
In any desktop operating system, there are several actions that the user can perform on application windows. For example, the user can move a window, minimize or maximize a window, close a window (and program), and so on. A common way to represent these different actions is with events. When the user does something, the program receives events from the operating system and can choose to respond to these events.
SDL manages an internal queue of events that it receives from the operating system. This queue contains events for many different window actions, as well as events related to input devices. Every frame, the game must poll the queue for any events and choose either to ignore or process each event in the queue. For some events, such as moving the window around, ignoring the event means SDL will just automatically handle it. But for other events, ignoring the event means nothing will happen.
Because events are a type of input, it makes sense to implement event processing in ProcessInput. Because the event queue may contain multiple events on any given frame, you must loop over all events in the queue. The SDL_PollEvent function returns true if it finds an event in the queue. So, a very basic implementation of ProcessInput would keep calling SDL_PollEvent as long as it returns true:
void Game::ProcessInput() { SDL_Event event; // While there are still events in the queue while (SDL_PollEvent(&event)) { } }
Note that the SDL_PollEvent function takes in an SDL_Event by pointer. This stores any information about the event just removed from the queue.
Although this version of ProcessInput makes the game window more responsive, the player still has no way to quit the game. This is because you simply remove all the events from the queue and don’t respond to them.
Given an SDL_Event, the type member variable contains the type of the event received. So, a common approach is to create a switch based on the type inside the PollEvent loop:
SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { // Handle different event types here } }
One useful event is SDL_QUIT, which the game receives when the user tries to close the window (either by clicking on the X or using a keyboard shortcut). You can update the code to set mIsRunning to false when it sees an SDL_QUIT event in the queue:
SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: mIsRunning = false; break; } }
Now when the game is running, clicking the X on the window causes the while loop inside RunLoop to terminate, which in turn shuts down the game and exits the program. But what if you want the game to quit when the user presses the Escape key? While you could check for a keyboard event corresponding to this, an easier approach is to grab the entire state of the keyboard with SDL_GetKeyboardState, which returns a pointer to an array that contains the current state of the keyboard:
const Uint8* state = SDL_GetKeyboardState(NULL);
Given this array, you can then query a specific key by indexing into this array with a corresponding SDL_SCANCODE value for the key. For example, the following sets mIsRunning to false if the user presses Escape:
if (state[SDL_SCANCODE_ESCAPE]) { mIsRunning = false; }
Combining all this yields the current version of ProcessInput, shown in Listing 1.4. Now when running the game, the user can quit either by closing the window or pressing the Escape key.
Listing 1.4 Game::ProcessInput Implementation
void Game::ProcessInput() { SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { // If this is an SDL_QUIT event, end loop case SDL_QUIT: mIsRunning = false; break; } } // Get state of keyboard const Uint8* state = SDL_GetKeyboardState(NULL); // If escape is pressed, also end loop if (state[SDL_SCANCODE_ESCAPE]) { mIsRunning = false; } }