Creating the Main Loop with SDL2

Step-by-step guide on creating the SDL2 application and event loops for interactive games
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
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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:

  1. Process any events, such as user input. This involves a second, nested loop often called the event loop.
  2. Ask all of our objects to update themselves, ready to be rendered to the screen. Any events that occurred in step 1 can influence this process
  3. Render a new frame to the screen, so the user can see the changes.
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.

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

Diagram showing an event queue

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.

Diagram showing an event loop interacting with an event queue

Creating the SDL Application Loop and 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) { 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.

Output Parameters

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:

  • Our function is updating a large object that the caller is providing
  • Our function is being called very frequently. SDL_PollEvent() is an example in this category.

Common Mistakes

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();
}

Handling Events

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:

  • Moving the mouse around our window
  • Clicking mouse buttons or pressing keyboard buttons
  • Resizing, moving and trying to close the window

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 in the next chapter. For now, let’s use these techniques to finally let users close our application.

Quitting the Application

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:

  • Introduce a shouldQuit variable, defaulted to false
  • Replace while(true) in our outer application loop with while(!shouldQuit)
  • Set 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

Using Attributes in the Event Loop

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
  // ...
}

Pushing Events and Custom 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.

The documentation for SDL_PushEvent is available here.

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.

Summary

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:

  • Understanding the Application Loop: We started by discussing the structure of the application loop. This loop is responsible for processing events, updating application state, and rendering.
  • Event Handling: The lesson provided a look at how events are managed in SDL2. We explored the event queue mechanism and demonstrated how to handle different types of events, including user input like keyboard and mouse events.
  • SDL Event Queue: We delved into the specifics of the SDL event queue, explaining how events are pushed to and popped from the queue using SDL_PollEvent().
  • Rendering: We also covered how to manage rendering in sync with the handling of events to maintain smooth visual output.
  • Best Practices and Common Mistakes: We discussed several best practices for setting up and using the SDL2 library, as well as common pitfalls to avoid, especially relating to event handling and frame rate management.

This lesson serves as a foundation for further exploration into more complex SDL2 functionalities, which we’ll cover throughout the rest of this course.

Was this lesson useful?

Next Lesson

Double Buffering

Learn the essentials of double buffering in C++ with practical examples and SDL2 specifics to improve your graphics projects
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
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

Learn the essentials of double buffering in C++ with practical examples and SDL2 specifics to improve your graphics projects
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved