Creating Custom Events

Learn how to create and manage your own game-specific events using SDL's event system.

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.

Starting Point

This lesson builds upon the foundation we previously established, featuring a main loop, window management (Window.h), and basic UI elements (UI.h, Rectangle.h, Button.h).

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

Using SDL_PushEvent()

To add our event to SDL's event queue, we use SDL_PushEvent(), passing a pointer to our SDL_Event. It will then show up in our event loop, like any other event.

In the following example, we push an SDL_QUIT event when a Button instance is left-clicked:

// Button.h
#pragma once
#include <SDL.h>
#include "Rectangle.h"

class UI;

class Button : public Rectangle {
 public:
  Button(UI& UIManager, const SDL_Rect& Rect)
  : Rectangle{Rect},
    UIManager{UIManager}
  {
    SetColor({255, 165, 0, 255});
  }

  void OnLeftClick() override {
    SDL_Event MyEvent{SDL_QUIT};
    SDL_PushEvent(&MyEvent);
  }

private:
  UI& UIManager;
};

We're currently constructing a button in our UIManager, which we can click on to verify our code works.

Event Storage Duration

The previous example may seem suspicious, as we're passing SDL_PushEvent() a pointer to a local variable. Passing points to local variables is something we should always be suspicious of, as that local SDL_Event object will be destroyed as soon as the OnLeftClick() function ends.

Therefore, we should wonder if the event we later receive in our event loop will be a dangling pointer.

In this case, we have nothing to worry about. Behind the scenes, SDL_PushEvent() copies the relevant data from the SDL_Event we provide. As such, 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 almost all 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() since SDL was initialized.

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

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

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

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

Sharing Custom Event Types

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:

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

namespace UserEvents{
  const inline Uint32 OPEN_SETTINGS{
    SDL_RegisterEvents(1)};
  const inline Uint32 CLOSE_SETTINGS{
    SDL_RegisterEvents(1)};
}

The inline specifier in this example allows us to #include this header file in multiple source files without violating the one definition rule. We covered namespaces, the one definition rule, and inline in more detail in this lesson in our introductory course:

Namespaces

Learn the essentials of using namespaces to organize your code and handle large projects with ease

Using Custom Events

Let's see a larger, more practical example of custom events in action. We'll have our Button to toggle a settings menu open and closed when clicked. Our program is very small and we could easily manage this without using the event queue, but let's stick with the contrivance so we can learn the concepts - they'll be important for larger projects.

A simple, but slightly flawed implementation of our Button might look like this:

// Button.h
// ...
#include "UserEvents.h"
// ...

class Button : public Rectangle {
 public:
  // ...

  void OnLeftClick() override {
    SDL_Event Event{isSettingsOpen
      ? UserEvents::CLOSE_SETTINGS
      : UserEvents::OPEN_SETTINGS
    };
    SDL_PushEvent(&Event);
    isSettingsOpen = !isSettingsOpen;
  }

private:
  UI& UIManager;
  bool isSettingsOpen{false};
};

The flaw here is that our Button assumes it is the only component that can open and close a settings menu. If there is another component that can dispatch similar events, the isSettingsOpen boolean within our Button will fall out of sync.

An improvement here would be to have our button both send and react to CLOSE_SETTINGS and OPEN_SETTINGS events. That could look like this:

// Button.h
// ...

class Button : public Rectangle {
 public:
  // ...
  
  void HandleEvent(SDL_Event& E) {
    Rectangle::HandleEvent(E);
    using namespace UserEvents;
    if (E.type == CLOSE_SETTINGS) {
      isSettingsOpen = false;
    } else if (E.type == OPEN_SETTINGS) {
      isSettingsOpen = true;
    }
  }

  void OnLeftClick() override {
    SDL_Event Event{isSettingsOpen
      ? UserEvents::CLOSE_SETTINGS
      : UserEvents::OPEN_SETTINGS
    };

    SDL_PushEvent(&Event);
    isSettingsOpen = !isSettingsOpen;
  }
  
  // ...
};

Now, when any object opens or closes the settings menu, every Button is notified through the HandleEvent() calls flowing through our hierarchy, and they can keep their individual isSettingsOpen values in sync.

A better design would be to remove these isSettingsOpen variables entirely, and store that state in a single location that is easily accessible to all interested parties. That might be our UIManager object, for example, but we'll stick with our event-based implementation for now as that's what we're focusing on.

Let's also create a hypothetical SettingsMenu element that will monitor these events. It will become active when it encounters an OPEN_SETTINGS event, and inactive when it encounters a CLOSE_SETTINGS event:

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

class SettingsMenu {
 public:
  void HandleEvent(SDL_Event& E) {
    if (E.type == UserEvents::OPEN_SETTINGS) {
      isOpen = true;
    }

    // If the settings menu isn't open, we ignore
    // all other events
    if (!isOpen) return;

    if (E.type == UserEvents::CLOSE_SETTINGS) {
      isOpen = false;
    }
    // Handle other events - mouse motion, mouse
    // buttons etc
    // ...
  }

  void Render(SDL_Surface* Surface) const {
    // Don't render if I'm not open
    if (!isOpen) return;
    
    SDL_FillRect(
      Surface,
      &Rect,
      SDL_MapRGB(
        Surface->format,
        Color.r, Color.g, Color.b
      ));
  }

 private:
  bool isOpen{false};
  SDL_Rect Rect{100, 50, 200, 200};
  SDL_Color Color{150, 150, 150, 255};
};

We can add both of these elements to our UI manager in the normal way:

#pragma once
#include <SDL.h>
#include "Button.h"
#include "SettingsMenu.h"

class UI {
public:
  void Render(SDL_Surface* Surface) const {
    SettingsButton.Render(Surface);
    Settings.Render(Surface);
  }

  void HandleEvent(SDL_Event& E) {
    SettingsButton.HandleEvent(E);
    Settings.HandleEvent(E);
  }

private:
  Button SettingsButton{*this, {50, 50, 50, 50}};
  SettingsMenu Settings;
};

Clicking our button should now toggle our hypthetical SettingsMenu open and closed:

User Event Data

Just like SDL's 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:

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

class SettingsMenu {
 public:
  void HandleEvent(SDL_Event& E) {
    if (E.type == UserEvents::OPEN_SETTINGS ||
        E.type == UserEvents::CLOSE_SETTINGS) {
      HandleUserEvent(E.user);
    }
  }

  // ...

 private:
  void HandleUserEvent(SDL_UserEvent& E) {
    std::cout << "That's a user event\n";
    using namespace UserEvents;
    if (E.type == OPEN_SETTINGS) {
      isOpen = true;
    } else if (E.type == CLOSE_SETTINGS) {
      isOpen = false;
    }
  }

  // ...
};
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 a pointer to 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.

The following approach will not work, as SomeInt is deleted as soon as OnLeftClick() ends, meaning data1 will be a dangling pointer:

// Button.h
// ...

class Button : public Rectangle {
 public:
  // ...

  void OnLeftClick() override {
    using namespace UserEvents;
    SDL_Event Event{
      isSettingsOpen
        ? CLOSE_SETTINGS
        : OPEN_SETTINGS
    };

    int SomeInt{42};
    Event.user.data1 = &SomeInt;

    SDL_PushEvent(&Event);

    // SomeInt is deleted here
  }
  // ...
};

Let's attach some more durable values. In the following example, both SomeInt and SomeFloat are member variables, so they remain alive as long as the Button remains alive.

In this program, our Button is a member of our UIManager, and our UIManager is a local variable of our main function. As such, it remains alive until our program ends, so we don't need to worry about these variables getting deleted too soon:

// Button.h
// ...

class Button : public Rectangle {
 public:
  // ...

  void OnLeftClick() override {
    using namespace UserEvents;
    SDL_Event Event{
      isSettingsOpen
        ? CLOSE_SETTINGS
        : OPEN_SETTINGS
    };

    Event.user.data1 = &SomeInt;
    Event.user.data2 = &SomeFloat;

    SDL_PushEvent(&Event);
  }

private:
  int SomeInt{42};
  float SomeFloat{9.8};
  // ...
};

Accessing Void Pointers and Type Safety

Over in SettingsMenu, let's read the values stored in the event. To meaningfully use a void pointer, we first need to statically cast it to the correct type:

// SettingsMenu.h
// ...

class SettingsMenu {
 public:
  // ...

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

    int* data1{static_cast<int*>(E.data1)};
    std::cout << "data1: " << *data1 << '\n';

    float* data2{static_cast<float*>(E.data2)};
    std::cout << "data2: " << *data2;

    using namespace UserEvents;
    if (E.type == OPEN_SETTINGS) {
      isOpen = true;
    } else if (E.type == CLOSE_SETTINGS) {
      isOpen = false;
    }
  }
  
  // ...
};
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:

// SettingsMenu.h
// ...

class SettingsMenu {
 // ...
 
 private:
  void HandleUserEvent(SDL_UserEvent& E) {
    std::cout << "That's a user event\n";

    int* data1{static_cast<int*>(E.data1)};
    std::cout << "data1: " << *data1 << '\n';

    int* data2{static_cast<int*>(E.data2)};
    std::cout << "data2: " << *data2;

    using namespace UserEvents;
    if (E.type == OPEN_SETTINGS) {
      isOpen = true;
    } else if (E.type == CLOSE_SETTINGS) {
      isOpen = false;
    }
  }
  
  // ...
};
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.

Constrained Dynamic Types using Unions and std::variant

Learn how to store dynamic data types in C++ using unions and the type-safe std::variant

Unconstrained Dynamic Types using Void Pointers and std::any

Learn how to use void pointers and std::any to implement unconstrained dynamic types, and understand when they should be used

However, in this case, we're forced to use void pointers, so we just have to be cautious.

The main strategy to mitigate this risk is to ensure that the types pointed at by data1 and data2 are consistent for any given event type. For example, we wouldn't want OPEN_SETTINGS events to sometimes have data1 pointing at an int, and sometimes pointing at a float. That unpredictability would make the data1 member very difficult to use for any component interested in the event.

Instead, every component that pushes OPEN_SETTINGS events onto the queue should by attaching the same type of data to the event. Then, any code that needs to react to that event knows that, if the event has a type of OPEN_SETTINGS, then the data1 and data2 types can be reliably inferred.

Storing Complex Types in User Events

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

Below, we add a new OpenSettingsConfig object, which lets the creator of an OPEN_SETTINGS request specify which part of the settings menu should be opened, and where it should be positioned:

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

namespace UserEvents{
  const inline Uint32 OPEN_SETTINGS{
    SDL_RegisterEvents(1)};
  const inline Uint32 CLOSE_SETTINGS{
    SDL_RegisterEvents(1)};

  enum class SettingsPage {
    GAMEPLAY, GRAPHICS, AUDIO
  };

  struct SettingsConfig {
    SettingsPage Page;
    int x;
    int y;
  };
}

Let's have our Button attach a pointer to a SettingsConfig object when it pushes an OpenSettings event:

// Button.h
#pragma once
#include <SDL.h>
#include "Rectangle.h"
#include "UserEvents.h"

class UI;

class Button : public Rectangle {
 public:
  // ...

  void OnLeftClick() override {
    using namespace UserEvents;
    SDL_Event Event{
      isSettingsOpen
        ? CLOSE_SETTINGS
        : OPEN_SETTINGS
    };

    if (Event.type == OPEN_SETTINGS) {
      Event.user.data1 = &Config;
    }

    SDL_PushEvent(&Event);
  }

private:
  UserEvents::SettingsConfig Config{
    UserEvents::SettingsPage::GAMEPLAY,
    50, 100
  };

  // ...
};

And let's have our SettingsMenu react to it:

// SettingsMenu.h
// ...

class SettingsMenu {
 public:
  // ...

 private:
  void HandleUserEvent(SDL_UserEvent& E) {
    using namespace UserEvents;
    if (E.type == OPEN_SETTINGS) {
      isOpen = true;

      auto* Config{
        static_cast<SettingsConfig*>(E.data1)
      };

      Rect.x = Config->x;
      Rect.y = Config->y;
      if (Config->Page == SettingsPage::GAMEPLAY) {
        std::cout << "Page: Gameplay Settings\n";
      }
    } else if (E.type == CLOSE_SETTINGS) {
      isOpen = false;
    }
  }

  bool isOpen{false};
  SDL_Rect Rect{100, 50, 200, 200};
  SDL_Color Color{150, 150, 150, 255};
};

Now, any component that requests the settings menu to open can control where it gets positioned, and what page it opens at:

Page: Gameplay Settings

Storing this in the User Event

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:

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

#include "Rectangle.h"
#include "UserEvents.h"

class UI;

class Button : public Rectangle {
 public:
  Button(UI& UIManager, const SDL_Rect& Rect)
  : Rectangle{Rect},
    UIManager{UIManager}
  {
    SetColor({255, 165, 0, 255});
  }

  void HandleEvent(SDL_Event& E) {
    Rectangle::HandleEvent(E);
    using namespace UserEvents;
    if (E.type == CLOSE_SETTINGS) {
      isSettingsOpen = false;
    } else if (E.type == OPEN_SETTINGS) {
      isSettingsOpen = true;
    }
  }

  void OnLeftClick() override {
    using namespace UserEvents;
    SDL_Event Event{
      isSettingsOpen
        ? CLOSE_SETTINGS
        : OPEN_SETTINGS
    };

    if (Event.type == OPEN_SETTINGS) {
      Event.user.data1 = this;
    }

    SDL_PushEvent(&Event);
  }

  UserEvents::SettingsConfig GetConfig() {
    return Config;
  }

  // Where is this button located?
  std::string GetLocation() {
    return "the main menu";
  }

private:
  UserEvents::SettingsConfig Config{
    UserEvents::SettingsPage::GAMEPLAY,
    50, 100
  };

  UI& UIManager;
  bool isSettingsOpen{false};
};

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

// SettingsMenu.h
#pragma once
#include <SDL.h>
#include <iostream>
#include "UserEvents.h"
#include "Button.h"

class SettingsMenu {
 // ...

 private:
  void HandleUserEvent(SDL_UserEvent& E) {
    using namespace UserEvents;
    if (E.type == OPEN_SETTINGS) {
      isOpen = true;

      auto* Instigator{
        static_cast<Button*>(E.data1)
      };

     std::cout << "I was opened from a button in "
        << Instigator->GetLocation() << "\n";

      Rect.x = Instigator->GetConfig().x;
      Rect.y = Instigator->GetConfig().y;
      if (
        Instigator->GetConfig().Page ==
          SettingsPage::GAMEPLAY
      ) {
        std::cout << "Page: Gameplay Settings\n";
      }
    } else if (E.type == CLOSE_SETTINGS) {
      isOpen = false;
    }
  }

  bool isOpen{false};
  SDL_Rect Rect{100, 50, 200, 200};
  SDL_Color Color{150, 150, 150, 255};
};
I was opened from a button in the main menu
Page: Gameplay Settings

We should always be mindful of the type safety concerns around void pointers. Above, our SettingsMenu assumes that data1 of a OPEN_SETTINGS event always points to a Button.

As such, any time we push an OPEN_SETTINGS event, from anywhere in our application, we should ensure that a Button pointer is included. If that's not viable, we need to rethink our design and devise an alternative way for the SettingsMenu to get the data it needs.

Complete Code

Here's the complete code incorporating custom events for opening and closing a settings menu, including passing data with the event.

Note that this specific implementation, while demonstrating custom events, won't be directly carried forward. Our next lessons on text and image rendering will start from a simpler base to focus on those new concepts.

Summary

In this lesson, we explored SDL's capability for handling custom, application-specific events.

We learned the process of registering new event types with SDL_RegisterEvents(), pushing these events using SDL_PushEvent(), and handling them within the standard event loop.

We also covered attaching custom data, such as configuration objects or this pointers, to events using the user field, facilitating decoupled communication patterns.

Key Takeaways:

  • SDL_RegisterEvents(1) reserves a unique Uint32 ID for a custom event.
  • SDL_PushEvent() adds a copy of your SDL_Event to the queue.
  • Custom events are processed in the main loop alongside standard SDL events.
  • The SDL_Event union's user member (SDL_UserEvent) holds custom data.
  • user.data1 and user.data2 are void pointers for arbitrary data.
  • Safe use of user.data1 and user.data2 requires careful type casting (static_cast) and lifetime management.
  • Custom events enable components to interact without direct dependencies.
  • Storing event type IDs (Uint32) in a shared header (e.g., UserEvents.h) improves organization.
Next Lesson
Lesson 27 of 129

Loading and Displaying Images

Learn how to load, display, and optimize image rendering in your applications

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

SDL Custom Event Registration Limit
Is there a limit to how many custom events I can register?
Using Smart Pointers (std::unique_ptr, std::shared_ptr) with SDL Custom Events
Instead of void pointers, can I use std::shared_ptr or std::unique_ptr with data1/data2? How?
Managing Data Lifetime for SDL Custom Events
How do I manage the lifetime of data pointed to by data1/data2 if the event might be processed much later?
SDL Custom Events vs. Building a Separate Event System
Why use SDL_UserEvent instead of just defining my own event system completely separate from SDL's?
Purpose of the code Member in SDL_UserEvent
The code member of SDL_UserEvent wasn't used much. What's its intended purpose?
Comparing SDL Custom Events to Signal/Slot Mechanisms (e.g., Qt)
How does this event system compare to signal/slot mechanisms in frameworks like Qt?
Implementing a Pause/Resume System with Custom Events
How can I use custom events to implement a pause/resume system in my game?
Efficiently Handling Multiple Custom Event Types
What's the best way to handle multiple custom event types without cluttering my main event loop?
Ensuring Thread Safety with Custom Events
How do I ensure thread safety when pushing custom events from multiple threads?
Prioritizing Custom Events in SDL
Is there a way to prioritize certain custom events over others in the SDL event queue?
Passing Complex Data in Custom SDL Events
What's the most efficient way to pass complex data structures through custom events?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant