This lesson explains the fundamental concept of the application loop – the engine that drives real-time programs. You'll learn how SDL manages events, how to poll the event queue to react to user input, and how to correctly structure your loop to handle window closing requests.
We’ll continue working on our main.cpp
and Window.h
files from earlier. Their current state is provided below.
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
while (true) {
SDL_PumpEvents();
}
SDL_Quit();
return 0;
}
// Window.h
#pragma once
#include <iostream>
#include <SDL.h>
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
SDL_FillRect(
GetSurface(),
nullptr,
SDL_MapRGB(
GetSurface()->format, 50, 50, 50
)
);
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() {
return SDL_GetWindowSurface(SDLWindow);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
private:
SDL_Window* SDLWindow{nullptr};
};
From a high level, we can imagine our main
function has three main areas. An area where we initialize things and create our objects, followed by a loop, followed by an area where we shut things down.
int main(int argc, char** argv) {
// Initialization
// ...
// Loop
while (true) {
// ...
}
// Shutdown
// ...
return 0;
}
These three components are the standard, high-level structure of all desktop applications, mobile apps, games, and any other type of program that is designed to continue running until the user asks it to close.
In this lesson, we’ll focus on the loop part. In this high level design, the loop that keeps our program running is often called the main loop, the application loop, or, if our program is a game, the game loop.
Within each iteration of the main loop, we perform three actions in order:
int main(int argc, char** argv) {
// Initialization
// ...
while (true) {
// 1. Process Events
// 2. Update Objects
// 3. Render Changes
}
// Shutdown
// ...
return 0;
}
If we design our application well and optimize the performance of its various components, our program can complete dozens or even hundreds of iteration of this loop every second.
This means that the player can perform some action and, within a few milliseconds, the effect of that action is visible on the screen. From a player’s perspective, a few milliseconds isn’t noticable - it may as well be instantenous, so we’ve created the illusion that they’re interacting with our program in "real time".
In this lesson, we’ll focus on the first part of the application loop - processing events. SDL uses an extremely common way of managing events, called an event queue.
An queue is a data structure, similar to an array, but designed in such a way that objects get added to the structure at one side, and removed at the other. Conceptually, an "event" is something that happened in our program, but in code, it’s just an object.
Like any object, it contains some member variables that help us understand what happened. SDL’s type for representing events is an SDL_Event
, which we’ll work with soon.
Adding an object to a queue is typically called "pushing" it, whilst removing and processing an event is often called "popping", respectively. We push events to the back of the queue, and pop them from the front:
To make our application react to the events that are happening, we repeatedly look at the front of the queue. If there’s an event there, we pop it from the queue, react accordingly to it, and then move on to the next event.
The code we write that repeatedly checks the front of the queue for events is an event loop. On every iteration, it pops an event, examines it, and reacts appropriately. It continues iterating until there are no more events in the queue.
Currently, the only thing in our application loop is a call to SDL_PumpEvents()
:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
// Initialization
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
while (true) {
// 1. Process Events
SDL_PumpEvents();
// 2. Update Objects
// ...
// 3. Render Changes
// ...
}
// Shutdown
SDL_Quit();
return 0;
}
This invocation tells SDL "we’re doing an iteration of our application loop right now - handle your events". Behind the scenes, SDL runs its event loop and processes all the events in its queue.
This is important - for SDL to work correctly, we must prompt it to process its events on every iteration of its application loop.
However, in most programs, we also want the opportunity to react to those events. If the user clicked their mouse somewhere in our window, that means they probably want our program to do something.
For visibility of events, we need to replace SDL_PumpEvents()
with our own event loop.
If we want to see what events are happening in our program, we can use SDL_PollEvent()
. This function pops the next event from SDL’s event queue, and lets us examine it.
To use SDL_PollEvent()
, we need to create an SDL_Event
object beforehand, and then pass it’s pointer to SDL_PollEvent()
:
SDL_Event Event;
SDL_PollEvent(&Event);
The SDL_PollEvent()
call will update our Event
object with details of the event that it popped off the queue. We can then examine that object and decide what action, if any, we need to take.
The design pattern where a function communicates with its caller by modifying an argument it provided is used very frequently in SDL’s API. This design is sometimes referred to as an output parameter, or out parameter. In C++, output parameters are implemented as non-const references or, in the case of SDL_PollEvent()
, a pointer to a non-const object.
When designing our own programs, output parameters are generally something to avoid. It’s more intuitive to have a function communicate with it’s caller by returning a value.
The main benefit of output parameters is performance - updating an existing object is often faster than creating a new one. So, we may want to consider using an output parameter design in scenarios where our function is updating some large object that the caller provides, or when our function is being called very frequently.
There are two additional things to note about SDL_PollEvent()
:
SDL_PollEvent()
returns true
if there was an event in the queue awaiting processing, and false
if the queue was emptySDL_PollEvent()
repeatedly on every iteration until it returns false
This means that an application loop that includes SDL_PollEvent()
will look something like this:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
// Initialization
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
// 1. Process Events
while (SDL_PollEvent(&Event)) {
// Examine the Event object and react
}
// 2. Update Objects
// ...
// 3. Render Changes
// ...
}
// Shutdown
SDL_Quit();
return 0;
}
That is, on every iteration of our application loop, we have an inner loop that continues until all of the outstanding events are processed - that is, until SDL_PollEvent()
returns false.
On every iteration of this inner event loop, we have the opportunity to react to an individual event. That is, the Event
object that we originally created, and that SDL_PollEvent()
has just updated with the event it popped from the queue. We’ll see how to react to events in the next section.
It’s fairly common for our initial attempts at application loops to be flawed. We may also not notice that something is wrong as, when our program is simple, a flawed application loop may seem to be working correctly.
Let’s take a look at two of the most common mistakes. First, we have this:
while(true) {
while(SDL_PollEvent(&Event)) {
// 1. Process Event
// 2. Update Objects
// 3. Render Changes
}
}
This loop forces the application loop and event loop to be in lockstep. This is problematic because, when no events are happening, our objects will not update. Many objects, such as those playing animations, want to be updating even when no events are happening.
Another example of a flawed application loop might look something like this:
while(true) {
SDL_PollEvent(&Event);
// 1. Process Event
// 2. Update Objects
// 3. Render Changes
}
The application fixes the previous problem - it will now update even when the event queue is empty. However, in this case, a maximum of one event can be processed on every iteration of our main loop.
When lots of events are happening, this means there can be a significant delay between each event and our application’s reaction to it. These delays make our program less responsive.
We should make sure our application loop follows the nested loop pattern we introduced in this lesson:
while(true) {
// 1. Process all the events
while(SDL_PollEvent(&Event)) {
// ....
}
// 2. Update Objects
// 3. Render Changes
}
This ensures our application loop will iterate even when no events are happening and, when multiple events occur in quick succession, all of them can be handled within the same iteration of our application loop.
SDL_PollEvent()
vs SDL_PumpEvents()
If we decide we no longer need to handle events, we shouldn’t just delete our event handling. Instead, we should go back to using SDL_PumpEvents()
.
For SDL to work correctly, we must prompt it to process its events at the appropriate time - that is, on every iteration of our application loop. If we don’t care what the events are, we should call SDL_PumpEvents()
:
while(true) {
// 1. Process all the events
SDL_PumpEvents();
// 2. Update Objects
// 3. Render Changes
}
If we do care, we should call SDL_PollEvent()
repeatedly until it returns false
:
while(true) {
// 1. Process all the events
while(SDL_PollEvent(&Event)) {
// ....
}
// 2. Update Objects
// 3. Render Changes
}
Now that we’re getting information on every event flowing through the system, it’s time to react to them. The first thing we need to understand is whether the event we’re currently processing is something we care about.
Our application loop can get quite complex, so if we have a lot of different things we need to react to, we should consider offloading that to a new function:
#include <SDL.h>
#include "Window.h"
void HandleEvent(SDL_Event& E) {
// ...
}
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
HandleEvent(Event);
}
}
SDL_Quit();
return 0;
}
SDL_Event
objects have a type
field, that let’s us get an initial understanding of what type of event we’re dealing with. SDL provides a range of helper values that we can compare against for specific event types we’re interested in. For example:
#include <SDL.h>
#include "Window.h"
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_MOUSEMOTION) {
std::cout << "Mouse moved\n";
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
std::cout << "Mouse clicked\n";
} else if (E.type == SDL_KEYDOWN) {
std::cout << "Keyboard button pressed\n";
}
}
int main(int argc, char** argv) {/*...*/}
If we move our mouse around and press some buttons, we should now see that activity being detected in our program:
Mouse moved
Mouse moved
Mouse clicked
Keyboard button pressed
Mouse moved
These various event types have further properties, letting us understand things like which button was pressed, or where the mouse moved to. We’ll cover the most useful event types throughout this course, but a full list is also available on the official documentation.
We finally know everything we need to know to let users close our application. When the player requests our game close by, for example, clicking the "x" in the title bar, SDL pushes an event onto the queue. It has a type of SDL_QUIT
:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
// User wants to quit...
}
}
}
SDL_Quit();
return 0;
}
To handle this, let’s add a new shouldContinue
boolean initialized to true
, and update our application loop from while(true)
to while(shouldContinue)
. When the player wants to quit, we’ll set shouldContinue
to false
, causing the loop to end:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
bool shouldContinue{true};
while (shouldContinue) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
shouldContinue = false;
}
}
}
SDL_Quit();
return 0;
}
As with anything, there are multiple ways we could have handled this. We could alternatively call SDL_Quit()
and return 0
from our application loop:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
bool shouldContinue{true};
while (true) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
}
SDL_Quit();
return 0;
}
SDL_QUIT
vs SDL_Quit
There may be some confusion here, as we're using two identifiers with very similar names in this lesson. C++ is case-sensitive, so the difference in their capitalization matters.
SDL_QUIT
is an event type, whilst SDL_Quit
is a similarly-named, but unrelated function that is used to request SDL clean up and shut itself down.
We’re not limited to just reading events from the SDL event queue. We can also add events to it, using SDL_PushEvent()
.
The SDL_Event
class has several constructors we can use. The most simple one accepts a single argument - the type of event we want to create, such as SDL_QUIT
.
Here’s an example of how we can push an SDL_QUIT
event onto the queue:
SDL_Event QuitEvent { SDL_QUIT };
SDL_PushEvent(&QuitEvent);
We can push an event like this from anywhere in our application. For example, we might have a custom quit button in our UI that could push this event. Then, once our event loop sees it, it will terminate the main loop and shut our program down.
We can also use the SDL event queue for custom event types, specific to our application. This might include events for when the player completes a level or gains a powerup.
We’ll be using this in our later projects as, whenever we’re managing a large collection of objects, an event queue is a useful pattern for letting disparate parts of our application communicate with each other.
Our latest files, which we’ve updated with the event loop and SDL_QUIT
handling, are below:
#include <SDL.h>
#include "Window.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
}
return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
800, 300, 0
);
SDL_FillRect(
GetSurface(),
nullptr,
SDL_MapRGB(
GetSurface()->format, 50, 50, 50
)
);
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() {
return SDL_GetWindowSurface(SDLWindow);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
private:
SDL_Window* SDLWindow{nullptr};
};
This lesson covered implementing the main application loop in SDL. We discussed the standard structure (initialize, loop, shutdown) and the loop's core tasks: handling events, updating game state, and rendering.
We focused on event handling, using SDL_PollEvent()
within a nested loop to process all events from SDL's queue and specifically reacting to the SDL_QUIT
event to enable application closure.
Key Takeaways:
SDL_PollEvent()
retrieves them.SDL_PollEvent()
to empty the event queue each frame.SDL_Event
objects contain details about each event, including its type.SDL_QUIT
to allow the application to close cleanly.SDL_PumpEvents()
or SDL_PollEvent()
in every iteration of your main loop.Step-by-step guide on creating the SDL2 application and event loops for interactive games
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games