Custom User Events

Discover how to use the SDL2 event system to handle custom, game-specific interactions
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

Previously, we’ve seen how SDL implements an event queue, which we can interact with using functions like SDL_PollEvent(). As standard, SDL will push events onto this queue to report actions like the user moving their mouse (SDL_MOUSEMOTION) or requesting the application close (SDL_QUIT).

However, we can also use SDL’s event system to manage custom events, that are specific to the game we’re making. For example, if we were making a first-person shooter, we could use this system to report when player fires their weapon or reloads.

In this lesson, we'll learn how to register and use custom events to create these game-specific behaviors.

We'll also look at how to organise our code around a custom event system, and see practical examples of how custom events can be used to manage game state and handle complex user interactions.

Creating and Pushing Events

An SDL_Event can be created like any other object:

SDL_Event MyEvent;

Its most useful constructor allows us to specify the type. Below, we create an event with a type of SDL_QUIT. This is equivalent to what SDL creates internally when the player attempts to close the window from the menu bar, for example:

#include <SDL.h>
#include <iostream>

int main(int argc, char** argv){
  SDL_Event MyEvent{SDL_QUIT};

  if (MyEvent.type == SDL_QUIT) {
    std::cout << "That's a quit event";
  }

  return 0;
}
That's a quit event

SDL_PushEvent()

To add our event to SDL’s event queue, we pass a pointer to it to SDL_PushEvent(). It will then be available to our main application loop, like any other event:

#include <iostream>
#include <SDL.h>

int main(int argc, char** argv){
  SDL_Init(SDL_INIT_EVENTS);
  SDL_Event Event;
  bool shouldQuit{false};

  while (!shouldQuit) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        std::cout << "Quitting";
        shouldQuit = true;
      }
    }
    SDL_Event MyEvent{SDL_QUIT};
    SDL_PushEvent(&MyEvent);
  }

  SDL_Quit();
  return 0;
}
Quitting

In real use cases, we’re going to be pushing events from some other part of our application. For example, our UI might be rendering an exit button somewhere, and it is some function in that class that will be creating and pushing the event:

#pragma once
#include <SDL.h>

class QuitButton : public Button {
  void HandleLeftClick() {
    SDL_Event QuitEvent{SDL_QUIT};
    SDL_PushEvent(&QuitEvent);
  }
};

Event Storage Duration

The previous example may seem suspicious, as we’re providing SDL_PushEvent() a pointer to a local variable. QuitEvent’s memory location will be freed as soon as HandleLeftClick() ends, so it would seem possible that the event we receive in our main loop will be a dangling pointer.

However, this is not the case. Behind the scenes, SDL_PushEvent() copies the relevant data from the SDL_Event we provide into the event queue, so we do not need to worry about our local copy of the event expiring before it is processed in our main loop

The event is copied into the queue, and the caller may dispose of the memory pointed to after SDL_PushEvent() returns.

Custom Events

Let’s see how we can now use SDL’s event system to handle custom events, whose type is specific to our program.

For example, our game might have a settings menu, and we’d like to use SDL’s event queue to report when the user requests to open that settings menu.

The first step of this process is to register our custom type with SDL. To do this, we call SDL_RegisterEvents(), passing an integer representing how many event types we want to register. In most cases, this will be 1:

// Register new event type
SDL_RegisterEvents(1);

This function will return a value that we can use as the type property when we create an SDL_Event. SDL event types are 32 bit unsigned integers and, behind the scenes, SDL_RegisterEvents() ensures that the integer it returns is unique.

That is, it does not conflict with any event types that SDL uses internally (such as SDL_QUIT) and it does not conflict with the type returned by any previous call to SDL_RegisterEvents()

Let’s save the value returned by our call, and use it to create an event:

#include <iostream>
#include <SDL.h>

int main(int argc, char** argv){
  SDL_Init(SDL_INIT_EVENTS);
  SDL_Event Event;
  bool shouldQuit{false};

  Uint32 OPEN_SETTINGS{SDL_RegisterEvents(1)};
  SDL_Event MyEvent{OPEN_SETTINGS};
  SDL_PushEvent(&MyEvent);

  while (!shouldQuit) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        shouldQuit = true;
      } else if (Event.type == OPEN_SETTINGS) {
        std::cout << "The player wants to open"
        " the settings menu\n";
      }
    }
  }

  SDL_Quit();
  return 0;
}
The player wants to open the settings menu

As before, in real use cases, events will typically not be dispatched from our main.cpp file - rather, they’ll be dispatched from some object deeper within our game, such as a button on the UI.

However, with custom event types, this adds a little complexity. The files that push the custom event type and the files that use it all need access to the Uint32 representing it’s type - the OPEN_SETTINGS variable in the previous example.

To accommodate this, we can move our event registrations to a header file, and #include it where needed. It may also be helpful to put these variables in a namespace, with a name like UserEvents:

//
#pragma once
#include <SDL.h>

namespace UserEvents{
  const inline Uint32 OPEN_SETTINGS{
    SDL_RegisterEvents(1)};
  const inline Uint32 CLOSE_SETTINGS{
    SDL_RegisterEvents(1)};
}
#pragma once
#include <SDL.h>
#include "UserEvents.h"

class ToggleSettingsButton: public Button {
  void HandleLeftClick() {
    SDL_Event Open{UserEvents::OPEN_SETTINGS};
    SDL_PushEvent(&Open);
  }
  void HandleRightClick() {
    SDL_Event Close{UserEvents::CLOSE_SETTINGS};
    SDL_PushEvent(&Close);
  }
};
#include <iostream>
#include <SDL.h>
#include "UserEvents.h"

int main(int argc, char** argv){
  SDL_Init(SDL_INIT_EVENTS);
  SDL_Event Event;
  bool shouldQuit{false};

  while (!shouldQuit) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        shouldQuit = true;
      } else if (Event.type ==
        UserEvents::OPEN_SETTINGS) {
        std::cout << "The player wants to open"
        " the settings menu\n";
      } else if (Event.type ==
        UserEvents::CLOSE_SETTINGS) {
        std::cout << "The player wants to close"
        " the settings menu\n";
      }
    }
  }

  SDL_Quit();
  return 0;
}

User Event Data

Just like the built in events can contain additional data (such as the x and y values of a SDL_MouseMotionEvent), so too can our custom events. Any event with a type returned from SDL_RegisterEvents() is considered a user event.

We can access the user event data from the user property of the SDL_Event. This will be a struct with a type of SDL_UserEvent:

#include <SDL.h>
#include <iostream>

void PushUserEvent(Uint32 EventType){
  SDL_Event MyEvent{EventType};
  SDL_PushEvent(&MyEvent);
}

void HandleUserEvent(SDL_UserEvent& E){
  std::cout << "That's a user event\n";
}

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_EVENTS);
  SDL_Event Event;
  bool shouldQuit{false};

  Uint32 OPEN_SETTINGS{SDL_RegisterEvents(1)};
  PushUserEvent(OPEN_SETTINGS);

  while (!shouldQuit) {
    while (SDL_PollEvent(&Event)) {
      if (Event.type == SDL_QUIT) {
        shouldQuit = true;
      } else if (Event.type == OPEN_SETTINGS) {
        HandleUserEvent(Event.user);
      }
    }
  }

  SDL_Quit();
  return 0;
}
That's a user event

An SDL_UserEvent struct has three members that we can attach data to:

  • code - a 32 bit integer
  • data1 - a void pointer (void*)
  • data2 - a void pointer (void*)

The two void pointers tend to be the most useful. A void pointer can point to any type of data, so we can store anything we might need in the data1 and data2 members.

Note, however, that we must ensure that the objects that these pointers point to remain alive long enough so anything that receives our event can make use of them.

Below, we attach a pointer to an int and a float to our event. Both variables are global, so they remain alive even after our PushUserEvent() call ends:

int SomeInt{42};
float SomeFloat{9.8f};

void PushUserEvent(Uint32 EventType){
  SDL_Event MyEvent{EventType};
  MyEvent.user.data1 = &SomeInt;
  MyEvent.user.data2 = &SomeFloat;
  SDL_PushEvent(&MyEvent);
}

Accessing Void Pointers and Type Safety

To meaningfully use a void pointer, we first need to statically cast it to the correct type:

void HandleUserEvent(SDL_UserEvent& E){
  std::cout << "That's a user event - data1: "
    << *static_cast<int*>(E.data1)
    << ", data2: "
    << *static_cast<float*>(E.data2) << '\n';
That's a user event - data1: 42, data2: 9.8

Void pointers are considered somewhat unsafe by modern standards, as the compiler is unable to verify that we’re casting to the correct type.

If we update our code with the mistaken belief that data2 is an int, we don’t get any compiler error. Instead, our program simply has unexpected behavior at run time:

void HandleUserEvent(SDL_UserEvent& E){
  std::cout << "That's a user event - data1: "
    << *static_cast<int*>(E.data1)
    << ", data2: "
    << *static_cast<int*>(E.data2) << '\n';
That's a user event - data1: 42, data2: 1092406477

We introduce modern, safer alternatives to void* in our advanced course, such as std::variant and std::any. However, in this case, we’re forced to use void pointers, so we just have to be cautious.

One obvious strategy that can be helpful here is to ensure that the types pointed at by data1 and data2 are consistent for any given event type. For example, we don’t want OPEN_SETTINGS events to sometimes have data1 pointing at an int, and sometimes pointing at a float.

Every component that pushes OPEN_SETTINGS events onto the queue should by attaching the same type of data to the event.

Storing this in the User Event

We’re not restricted to storing simple primitive types in the data1 or data2 pointers. We can store pointers to any data type, including custom data types created specifically to support the respective event type, if needed.

A common pattern is for the event to include a pointer to the object that created it. This is particularly useful when we have multiple instances of some class that can push events. Code that handles those events will often need to know which specific instance the event came from.

We can do this using the this pointer:

#pragma once
#include <SDL.h>
#include "UserEvents.h"

class OpenSettingsButton: public Button {
public:
  std::string GetLocation() {
    return "Sidebar";
  }
  int GetId() {
    return 42;
  }
  void HandleLeftClick() {
    SDL_Event Event{UserEvents::OPEN_SETTINGS};
    Event.user.data1 = this;
    SDL_PushEvent(&Event);
  }
};

This allows any function that receives the event to access the public methods of the object that sent it, which can be helpful for determing how it needs to react:

void HandleUserEvent(SDL_UserEvent& E){
  if (E.type != UserEvents::OPEN_SETTINGS) {
    return;
  }
  auto Button = static_cast<OpenSettingsButton*>(
    E.data1
  );
  std::cout << "Open Settings Event Received\n"
    << "Button Location: "
    << Button->GetLocation()
    << ", id: " << Button->GetId() << '\n';
}
Open Settings Event Received
Button Location: Sidebar, id: 42

Summary

In this lesson, we explored how to create and manage custom events in SDL2. We learned how to create and push events onto SDL's event queue using SDL_PushEvent(). We then delved into creating custom event types using SDL_RegisterEvents() and how to handle these events in our main loop.

The lesson also covered how to attach additional data to user events using the SDL_UserEvent struct, including the common pattern of storing a pointer to the object that created the event.

Was this lesson useful?

Next Lesson

Loading and Displaying Images

Learn how to load, display, and optimize image rendering in your applications
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Custom User Events

Discover how to use the SDL2 event system to handle custom, game-specific interactions

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
Implementing User Interaction
  • 31.Engine Overview
  • 32.Creating the Grid
  • 33.Placing the Bombs
  • 34.Handling Adjacent Cells
  • 35.Ending and Restarting Games
  • 36.Placing Flags
  • 41.GPUs and Rasterization
  • 42.SDL Renderers
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:

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

Loading and Displaying Images

Learn how to load, display, and optimize image rendering in your applications
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved