The first step of implementing any application is writing the foundational code that keeps the program running until we or the user decide it’s time to quit. This code is often called the application loop, main loop, or, if we’re making a game, the game loop.
Typically, every iteration of the loop involves 3 steps:
int main() {
while(true) {
// 1. Handle Events
while(GetUnhandledEvent()) {
// Process event
// ...
}
// 2. Update Objects
// ...
// 3. Render Frame
// ...
}
}
Each iteration of this outer loop happens extremely quickly. The faster each iteration runs, the higher our application’s frame rate will be. Higher frame rates make our application feel more responsive to user input.
For example, the minimum frame rate that is typically considered acceptable is 30 frames per second. To achieve that, we need to complete each iteration of our loop within 33 milliseconds - that is 1000ms / 30.
If we want 60 frames per second, we only have 16 milliseconds per frame: 1000ms / 60.
In this lesson and the next, we’ll focus on the "process any events" part of the application loop. We’ll cover updating objects and rendering them to the screen in the following chapters.
It’s worth exploring this event-handling aspect a little deeper. SDL uses an extremely common way of managing events, called an event queue.
An event 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.
These actions are typically called "pushing" and "popping", respectively. We push events to the back of the queue, and pop them from the front:
Any time an event occurs, an object representing that event gets added to the back of this queue.
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 delete it.
The code we write that repeatedly checks the front of the queue for events is an event loop.
Let's take a look at a minimalist application loop using SDL2:
#include <SDL.h>
class Window {/*...*/};
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
SDL_Event Event;
// Application Loop
while(true) {
// Event Loop
while(SDL_PollEvent(&Event)) {
// 1. Handle Event
// ...
}
// 2. Update Objects
// ...
// 3. Render Frame
GameWindow.RenderFrame();
}
SDL_Quit();
return 0;
}
Note that this example is using the Window
class and related techniques we covered in the previous lesson:
We’re handling events in our main
function, and the two key components are the SDL_Event
object and the SDL_PollEvent()
function call. These are how we interact with SDL’s event queue.
SDL_Event
is the base class for all of SDL’s events. It is the type of objects that are on the event queue.SDL_PollEvent()
is the method whereby we remove the next element in the queue, so we can process it.To implement SDL_PollEvent()
, we must first create an SDL_Event
object and then pass its pointer into SDL_PollEvent()
.
SDL will then update our object with the details of the next event we need to handle, and remove that event from the queue.
When there are no more events to process, SDL_PollEvent()
will return 0
. So, we can use that return value to determine when our event loop ends for this frame. The official documentation for SDL_Event
is available here.
The design pattern where a function communicates with its caller by modifying an argument it provided is sometimes referred to as an output parameter, or out parameter. In C++, this is implemented as a parameter that is a non-const reference, or a pointer to a non-const.
When creating our own functions, out parameters are not something we should use often - its generally more intuitive to have a function communicate with it’s caller by returning a value.
The main benefit of out parameters is performance - updating an existing object is typically more performant than creating a new one. So, we may want to consider setting up our function to use an out parameter in one of two scenarios:
SDL_PollEvent()
is an example in this category.At first glance, it seems we could simplify our application loop to be this:
while(SDL_PollEvent(&Event)) {
// 1. Handle Event
// 2. Update Everything
// 3. Render Frame
}
If we tried it, it would even seem to work. However, this loop forces the frame rate and event loop to be in lockstep. This is problematic for two reasons.
Firstly, when fewer events are happening, the application will update more slowly. This is generally undesirable as many objects, such as those playing animations, typically want to be updated regardless of how many events are happening.
We could fix that problem by changing their application loop to be something like this:
while(true) {
SDL_PollEvent(&Event);
// 1. Handle Event
// 2. Update Everything
// 3. Render Frame
}
The application will now output frames as fast as possible. But now, a maximum of one event is being processed per frame.
When lots of events are happening, it can take many frames before user input is reflected on the screen. This can make our applications feel unresponsive.
As such, our application loop should typically update frames as fast as possible, with an inner loop that can process multiple events in each frame:
while(true) {
// Handle all the events
while(SDL_PollEvent(&Event)) {
// ...
}
// Update The Frame
GameWindow.RenderFrame();
}
Once SDL_PollEvent()
has updated our event object, it’s time for us to examine that object. We can then determine what action, if any, we need to take.
The first thing we need to do is determine what type of event it is. SDL events have a type
property for this:
#include <SDL.h>
#include <iostream>
class Window {/*...*/};
int main(int argc, char** argv) {
Window GameWindow;
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
std::cout << "\nType: " << Event.type;
}
GameWindow.RenderFrame();
}
SDL_Quit();
return 0;
}
We can now run our program and generate some events by performing actions like:
Going so will stream output like the following to our terminal:
Type: 512
Type: 770
Type: 512
Type: 1024
SDL provides us with variables to help us understand what these integers represent. For example, we can determine if an event represents mouse movement by comparing its type
to the SDL_MOUSEMOTION
variable.
Event.type == SDL_MOUSEBUTTONDOWN
We can use this technique to expand our event handling loop with conditional logic, allowing us to programmatically react to different event types:
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_MOUSEMOTION) {
std::cout << "Mouse moved\n";
} else if (Event.type == SDL_MOUSEBUTTONDOWN) {
std::cout << "Mouse clicked\n";
} else if (Event.type == SDL_KEYDOWN) {
std::cout << "Keyboard button pressed\n";
}
}
Mouse moved
Mouse moved
Mouse clicked
Keyboard button pressed
Mouse moved
We’ll go into much more detail on these events when we cover user interaction in the next chapter. For now, let’s use these techniques to finally let users close our application.
SDL_PumpEvents()
In some programs, we don’t need to react to any events coming from SDL, so we don’t need to implement an event loop using SDL_PollEvent()
.
However, even if we’re not using events in our own code, SDL internally depends on the event queue being processed. If we’re not using SDL_PollEvent()
, we still need to prompt SDL to process its events at the appropriate time within our application loop.
To do this, we can replace the event loop with a call to SDL_PumpEvents()
:
// Application Loop
while(true) {
while(SDL_PollEvent(&Event)) {
// ...
}
SDL_PumpEvents();
// Update The Frame
GameWindow.RenderFrame();
}
Typically, the first event we’ll want to handle is SDL_QUIT
. When we encounter this event, it means something has requested our application close. For example, the user may have clicked the x
button on our window’s title bar.
SDL_QUIT
vs SDL_Quit()
There may be some confusion here, as we've used two identifiers with very similar names in this lesson. C++ is case-sensitive, so the difference in their capitalization matters.
SDL_QUIT
is an integer that matches an event type. SDL_Quit
is a similarly-named, but unrelated function that is used to request SDL clean up and shut itself down.
When an attempt is made to close our application, SDL pushes an event to the queue for us to react to:
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
std::cout << "User wants to quit";
}
// ... handle other event types
}
Running our application and trying to quit, we should now see the output:
User wants to quit
Let’s update our program to handle this. We can do this in three steps:
shouldQuit
variable, defaulted to false
while(true)
in our outer application loop with while(!shouldQuit)
shouldQuit
to true
in our event loop, when we detect the user requested to quit.Putting everything together, it looks like this:
#include <SDL.h>
#include <iostream>
class Window {/*...*/};
int main(int argc, char** argv) {
Window GameWindow;
SDL_Event Event;
bool shouldQuit{false};
while (!shouldQuit) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
std::cout << "User wants to quit\n";
shouldQuit = true;
}
// ... handle other event types
}
GameWindow.RenderFrame();
}
std::cout << "Goodbye world";
SDL_Quit();
return 0;
}
Running our program and pressing the quit button, we should see the output we defined, and our program should quit successfully:
User wants to quit
Goodbye world
Almost by definition, the SDL_QUIT
branch of our event loop is only going to happen once. Other events, such as mouse motion, could happen millions of times in our event loop.
As such, we may want to add attributes such as [[unlikely]]
to our event loop, providing the compiler with some context that it can use to make optimization decisions:
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) [[unlikely]] {
// Handle Quit
// ...
}
// Handle other event types
// ...
}
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 simply 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 trigger this event. Then, once our event loop sees it, it will react accordingly.
We can also use the SDL event queue for custom event types, specific to our application, using SDL_UserEvent
and SDL_RegisterEvents()
. We’ll use these later in the course when we start implementing our gameplay logic.
In this lesson, we set up the foundations for developing an SDL2-based application, focusing on creating an application loop. Here’s a recap of the key topics covered:
SDL_PollEvent()
.This lesson serves as a foundation for further exploration into more complex SDL2 functionalities, which we’ll cover throughout the rest of this course.
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