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.
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);
}
};
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.
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;
}
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 integerdata1
- 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);
}
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.
this
in the User EventWe’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
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.
Discover how to use the SDL2 event system to handle custom, game-specific interactions
Apply what we learned to build an interactive, portfolio-ready capstone project using C++ and the SDL2 library
Apply what we learned to build an interactive, portfolio-ready capstone project using C++ and the SDL2 library
Free, unlimited access