Implementing an SDL2 Application Loop in C++

Setting solid foundations for our application, by implementing the main loop and the event loop
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
aSDL6.jpg
Ryan McCombe
Ryan McCombe
Posted

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, or game loop, when we’re making games.

Typically, every iteration of the loop does three things:

  1. Process any events, such as user input
  2. Ask all of our objects to update themselves, ready to be rendered to the screen. This process can factor in any events that occurred
  3. Render a new frame to the screen, so the user can see the changes.

Each iteration of this loop happens extremely quickly.

The faster each iteration of this loop runs, the higher our application’s frame rate will be. Higher frame rates make our application feel more responsive to user input.

As a frame of reference, the minimum frame rate that is typically considered acceptable is 30 frames per second. To hit that, we need to be completing each iteration of our loop within 33 milliseconds. If we want 60 frames per second, we only have 16 milliseconds per frame.

The Event Queue and Event Loop

In this lesson and the next, we’ll focus on the "process any events" part of the application loop. We’ll cover updating and rendering in the following two lessons.

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 end, and removed at the other.

These actions are typically called "pushing" and "popping", respectively

Diagram showing the event queue

Any time an event occurs, an object representing that event gets added to the end of this queue.

To make our application react to the events that are happening, we constantly look at the start of the queue. If there’s an event there, we remove it from the queue, react accordingly to it, then delete it.

The code we write that constantly checks the front of the queue for events is an event loop.

Diagram showing the event loop

Creating the Application Loop and Event Loop in SDL2

Let's take a look at what a typical application loop might look like when we’re using SDL2:

#include <SDL.h>
#include "Window.h"

int main() {
  Window GameWindow;
  SDL_Event Event;

  // Application Loop
  while(true) {
    // Event Loop
    while(SDL_PollEvent(&Event)) {
      // 1. Handle Events
    }
    // 2. Update Everything
    // 3. Render Frame
  }
}

The two new things to note here are the SDL_Event object and the SDL_PollEvent function call. These two things 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 object 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.

The implementation of SDL_PollEvent requires us to create an SDL_Event object ahead of time, 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.

Alternative Application Loops

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. Many applications are even using setups like this. But, it forces the frame rate and event loop to be in lockstep. This is problematic in two scenarios.

Firstly, when relatively few events are happening, the application will update slower. This is generally undesirable - we want to do things like update animations at a pretty consistent frame rate.

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, the second problem is still there: a maximum of one event can be processed per frame.

When lots of events are happening, it can take several frames before user input is reflected on the screen. This can make our applications feel unresponsive.

So, modern application loops treat event handling and frame rendering as two unlinked processes, to the extent that is possible.

Handling SDL2 Events in C++

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.

Typically, the first thing we need to do is determine what type of event it is. Events have a type property for this.

while (SDL_PollEvent(&Event)) {
  if (Event.type == /* ?? */) {
    // ...
  } else if (Event.type == /* ?? */) {
    // ...
  } else if (Event.type == /* ?? */) {
    // ...
  }
}

The type member on the SDL_Event class is an integer. However, SDL provides some helpers that maps those integers to meaningful names, so our code is easier to read.

Lets see an example.

Quitting C++ SDL2 Applications: the SDL_QUIT Event

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.

If our event loop is being run in the main function, we can honor this request with a simple return statement:

while (SDL_PollEvent(&Event)) {
  if (Event.type == SDL_QUIT) [[unlikely]] {
    return 0;
  } else if (Event.type == /* ?? */) {
    // ...
  } else if (Event.type == /* ?? */) {
    // ...
  }
}

We’ve added the [[unlikely]] attribute here. This is because the event loop is somewhere we really want to be conscious of performance. It’s unlikely any given event is the quit event because, almost by definition, that only happens once.

Finally, we can now close our application without having to force it!

Tip: Call SDL_Quit() before Closing

It’s generally recommended to inform SDL we’re about to quit our application.

We do that by calling SDL_Quit() right before our application ends. This allows SDL to do any necessary cleanup. For example, it might need to revert any system settings that we changed from our application.

while (SDL_PollEvent(&Event)) {
  if (Event.type == SDL_QUIT) [[unlikely]] {
    SDL_Quit();
    return 0;
  }
  //...
}

The official SDL documentation for SDL_Quit is available here.

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 capitalisation 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.

Pushing Events in SDL2 (C++)

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. Then, once our main event loop sees it, it will react accordingly.

The documentation for SDL_PushEvent is available here.

We can also use the SDL event queue for custom event types, specific to our application. That goes beyond the scope of this lesson, but if it’s of interest, SDL_UserEvent and SDL_RegisterEvents are good starting points to investigate.

In the next lesson, we’ll see examples of how we can capture and react to user input - keyboard and mouse events.

Was this lesson useful?

Next Lesson

Double Buffering

An introduction to double buffering - the foundational technique behind real time graphics
aSDL7.jpg
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 27 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Double Buffering

An introduction to double buffering - the foundational technique behind real time graphics
aSDL7.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved