Implementing an Application Loop

Step-by-step guide on creating the SDL3 application and event loops.

Ryan McCombe
Updated

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.

Starting Point

In this lesson, we'll continue working on our main.cpp and Window.h files from earlier. We'll specifically be focusing on our main function, explaining and breaking down the code we have currently implemented there.

Our main.cpp and Window.h are provided below for reference:

Files

src
Select a file to view its content

The Application Loop

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, char**) {
  // 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 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:

  1. We react to any events that happened, such as the user pressing keyboard buttons and clicking on things
  2. We update the objects that our program is managing
  3. We render some visual output to the screen so the user can see the changes
int main(int, char**) {
  // 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 iterations 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 noticeable - it may as well be instantaneous, so we've created the illusion that they're interacting with our program in "real time".

Events

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.

A 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" it. 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.

Handling Events

For SDL to work correctly, we must prompt it to process its events on every iteration of the application loop. The simplest way to do this is with SDL_PumpEvents():

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"

int main(int, char**) {
  // 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 prompts SDL to handle its events internally but, in most programs, we want our code to have the opportunity to react to those events, too. If the user clicked their mouse somewhere in our window, that means they probably want our program to do something - particularly if they clicked their mouse at a location where we're rendering a button.

For visibility of events, we can replace SDL_PumpEvents() with our own event loop.

Creating an 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 its 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.

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 empty
  • Multiple events can happen within the same iteration of our main application loop, so we need to call SDL_PollEvent() repeatedly on every iteration until it returns false

This means that an application loop that includes SDL_PollEvent() will look something like this:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"

int main(int, char**) {
  // 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 data that it popped from the queue. We'll see how to react to events in the next section.

Common Mistakes

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
}

This 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)) {
    // Process an individual 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.

Comparing SDL_PollEvent() and 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)) {
    // React to each event....
  }

  // 2. Update Objects
  // 3. Render Changes
}

Event Types

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, with a name like HandleEvent():

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"

void HandleEvent(SDL_Event& E) {
  // ... 
}

int main(int, char**) {
  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 we can inspect to 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:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "Window.h"

void HandleEvent(SDL_Event& E) {
  if (E.type == SDL_EVENT_MOUSE_MOTION) {
    std::cout << "Mouse moved\n";
  } else if (E.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
    std::cout << "Mouse clicked\n";
  } else if (E.type == SDL_EVENT_KEY_DOWN) {
    std::cout << "Keyboard button pressed\n";
  }
}

int main(int, char**) {/*...*/}

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 in the official documentation.

Mouse Focus and Input Focus

By default, SDL only reports mouse events when the mouse is hovering over our window - that is, when our window has mouse focus.

Similarly, SDL only reports keyboard events if the window is active - that is, when our window has input focus. On desktop platforms, you can typically control which window is active simply by clicking on it.

We cover mouse focus and input focus in more detail later in the course.

Quitting the Application

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_EVENT_QUIT:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event Event;
  while (true) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_EVENT_QUIT) {
        // User wants to quit...
      }
    }
  }

  SDL_Quit();
  return 0;
}

To handle this, let's add a new IsRunning boolean initialized to true, and update our application loop from while(true) to while(IsRunning). When the player wants to quit, we'll set IsRunning to false.

This causes our main loop to stop iterating, which means our main function will soon end and our program will close:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event Event;
  bool IsRunning{true};
  while (IsRunning) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_EVENT_QUIT) {
        IsRunning = 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:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include "Window.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;

  SDL_Event Event;
  while (true) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_EVENT_QUIT) {
        SDL_Quit();
        return 0;
      }
    }
  }

  // This part of the code is now unreachable
  // but can be left for clarity
  SDL_Quit();
  return 0;
}

Complete Code

We've provided the complete code below for reference. This is the same code we had at the start of the lesson, but we've now rebuilt it line-by-line. A thorough understanding of the application loop is the foundation of all real-time program:

Files

src
Select a file to view its content

Summary

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 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_EVENT_QUIT event to enable application closure.

Key Takeaways:

  • Desktop applications rely on a continuous main loop.
  • The loop structure is typically: Process Events -> Update -> Render.
  • Events are managed in a queue; SDL_PollEvent() retrieves them.
  • Use a while loop with SDL_PollEvent() to empty the event queue on each iteration of the application loop.
  • SDL_Event objects contain details about each event, including its type.
  • Handle SDL_EVENT_QUIT to allow the application to close cleanly.
  • Remember to call SDL_PumpEvents() or SDL_PollEvent() in every iteration of your main loop.
Next Lesson
Lesson 20 of 25

Double Buffering

Learn the essentials of double buffering in C++ with practical examples and SDL3 specifics

Have a question about this lesson?
Answers are generated by AI models and may not be accurate