Creating Custom Events

Learn how to create and manage your own game-specific events using SDL's event system.
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

Get Started for Free
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.

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

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

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;
  UI UIManager;

  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      UIManager.HandleEvent(E);
      if (E.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    GameWindow.Render();
    UIManager.Render(GameWindow.GetSurface());
    GameWindow.Update();
  }

  return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      700, 300, 0
    );
  }

  void Render() {
    SDL_FillRect(
      GetSurface(),
      nullptr,
      SDL_MapRGB(
        GetSurface()->format, 50, 50, 50
      )
    );
  }

  void Update() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() const {
    return SDL_GetWindowSurface(SDLWindow);
  }

  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;
  ~Window() {
    if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
      SDL_DestroyWindow(SDLWindow);
    }
  }

private:
  SDL_Window* SDLWindow{nullptr};
};
#pragma once
#include <SDL.h>
#include "Rectangle.h"
#include "Button.h"

class UI {
public:
  void Render(SDL_Surface* Surface) const {
    A.Render(Surface);
    B.Render(Surface);
    C.Render(Surface);
  }

  void HandleEvent(SDL_Event& E) {
    A.HandleEvent(E);
    B.HandleEvent(E);
    C.HandleEvent(E);
  }

private:
  Rectangle A{{50, 50, 50, 50}};
  Rectangle B{{150, 50, 50, 50}};
  Button C{*this, {250, 50, 50, 50}};
};
#pragma once
#include <SDL.h>

class Rectangle {
 public:
  Rectangle(const SDL_Rect& Rect)
  : Rect{Rect} {}

  void Render(SDL_Surface* Surface) const {
    auto [r, g, b, a]{
      isPointerHovering ? HoverColor : Color
    };
    SDL_FillRect(
      Surface, &Rect,
      SDL_MapRGB(Surface->format, r, g, b)
    );
  }

  virtual void OnMouseEnter() {}
  virtual void OnMouseExit() {}
  virtual void OnLeftClick() {}

  void HandleEvent(SDL_Event& E) {
    if (E.type == SDL_MOUSEMOTION) {
      bool wasPointerHovering{isPointerHovering};
      isPointerHovering = isWithinRect(
        E.motion.x, E.motion.y
      );
      if (!wasPointerHovering && isPointerHovering) {
        OnMouseEnter();
      } else if (
        wasPointerHovering && !isPointerHovering
      ) {
        OnMouseExit();
      }
    } else if (
      E.type == SDL_WINDOWEVENT &&
      E.window.event == SDL_WINDOWEVENT_LEAVE
    ) {
      if (isPointerHovering) OnMouseExit();
      isPointerHovering = false;
    } else if (E.type == SDL_MOUSEBUTTONDOWN) {
      if (isPointerHovering &&
        E.button.button == SDL_BUTTON_LEFT
      ) {
        OnLeftClick();
      }
    }
  }

  void SetColor(const SDL_Color& NewColor) {
    Color = NewColor;
  }

  SDL_Color GetColor() const {
    return Color;
  }

  void SetHoverColor(const SDL_Color& NewColor) {
    HoverColor = NewColor;
  }

  SDL_Color GetHoverColor() const {
    return HoverColor;
  }

private:
  SDL_Rect Rect;
  SDL_Color Color{255, 0, 0};
  SDL_Color HoverColor{0, 0, 255};

  bool isPointerHovering{false};

  bool isWithinRect(int x, int y) {
    if (x < Rect.x) return false;
    if (x > Rect.x + Rect.w) return false;
    if (y < Rect.y) return false;
    if (y > Rect.y + Rect.h) return false;
    return true;
  }
};
#pragma once
#include <SDL.h>
#include "Rectangle.h"

class UI;

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

  UI& UIManager;
};

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 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 the pointer we’re passing to SDL_PushEvent() is to a local variable. That local SDL_Event object will be destroyed as soon as the OnLeftClick() function ends.

Therefore, we’re right to wonder if the event we later receive in our event loop will be a dangling pointer.

Fortunately, this is not the case. 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 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() 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)};
}

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:

Screenshot showing our Button and Settings menu

Design Pattern: Decoupling

In the previous lesson, we talked about the dangers of having two components directly communicating with each other. When two components are connected together (by, for example, one storing a reference to the other), those components are considered tightly coupled. If we rely too much on coupling arbitrary components together, our project becomes difficult to manage.

However, in real projects, our components still need ways to interact with each other. Our previous example shows how an event queue can help us achieve that, without the interacting components becoming coupled.

Our SettingsButton can open our SettingsMenu, even though neither component knows the other exists. One is just pushing events onto the queue without needing to know which (if any) components care about it. The other is monitoring queue events to check if any are interesting, without needing to know where those events came from.

If another component becomes interested in the settings menu opening and closing, we don’t need to update any of our existing components. That new component just needs to watch the event queue for relevant events, and react accordingly when it encounters them.

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.

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.

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
Screenshot showing the repositioned settings menu

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.

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

int main(int argc, char** argv) {
  SDL_Init(SDL_INIT_VIDEO);
  Window GameWindow;
  UI UIManager;

  SDL_Event E;
  while (true) {
    while (SDL_PollEvent(&E)) {
      UIManager.HandleEvent(E);
      if (E.type == SDL_QUIT) {
        SDL_Quit();
        return 0;
      }
    }

    GameWindow.Render();
    UIManager.Render(GameWindow.GetSurface());
    GameWindow.Update();
  }

  return 0;
}
#pragma once
#include <iostream>
#include <SDL.h>

class Window {
public:
  Window() {
    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED,
      700, 300, 0
    );
  }

  void Render() {
    SDL_FillRect(
      GetSurface(),
      nullptr,
      SDL_MapRGB(
        GetSurface()->format, 50, 50, 50
      )
    );
  }

  void Update() {
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() const {
    return SDL_GetWindowSurface(SDLWindow);
  }

  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;
  ~Window() {
    if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
      SDL_DestroyWindow(SDLWindow);
    }
  }

private:
  SDL_Window* SDLWindow{nullptr};
};
#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;
};
#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};
};
#pragma once
#include <SDL.h>

class Rectangle {
 public:
  Rectangle(const SDL_Rect& Rect)
  : Rect{Rect} {}

  void Render(SDL_Surface* Surface) const {
    auto [r, g, b, a]{
      isPointerHovering ? HoverColor : Color
    };
    SDL_FillRect(
      Surface, &Rect,
      SDL_MapRGB(Surface->format, r, g, b)
    );
  }

  virtual void OnMouseEnter() {}
  virtual void OnMouseExit() {}
  virtual void OnLeftClick() {}

  void HandleEvent(SDL_Event& E) {
    if (E.type == SDL_MOUSEMOTION) {
      bool wasPointerHovering{isPointerHovering};
      isPointerHovering = isWithinRect(
        E.motion.x, E.motion.y
      );
      if (!wasPointerHovering && isPointerHovering) {
        OnMouseEnter();
      } else if (
        wasPointerHovering && !isPointerHovering
      ) {
        OnMouseExit();
      }
    } else if (
      E.type == SDL_WINDOWEVENT &&
      E.window.event == SDL_WINDOWEVENT_LEAVE
    ) {
      if (isPointerHovering) OnMouseExit();
      isPointerHovering = false;
    } else if (E.type == SDL_MOUSEBUTTONDOWN) {
      if (isPointerHovering &&
        E.button.button == SDL_BUTTON_LEFT
      ) {
        OnLeftClick();
      }
    }
  }

  void SetColor(const SDL_Color& NewColor) {
    Color = NewColor;
  }

  SDL_Color GetColor() const {
    return Color;
  }

  void SetHoverColor(const SDL_Color& NewColor) {
    HoverColor = NewColor;
  }

  SDL_Color GetHoverColor() const {
    return HoverColor;
  }

private:
  SDL_Rect Rect;
  SDL_Color Color{255, 0, 0};
  SDL_Color HoverColor{0, 0, 255};

  bool isPointerHovering{false};

  bool isWithinRect(int x, int y) {
    if (x < Rect.x) return false;
    if (x > Rect.x + Rect.w) return false;
    if (y < Rect.y) return false;
    if (y > Rect.y + Rect.h) return false;
    return true;
  }
};
#pragma once
#include <SDL.h>
#include <iostream>
#include "UserEvents.h"
#include "Button.h"

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

  void Render(SDL_Surface* Surface) const {
    if (!isOpen) return;
    SDL_FillRect(
      Surface,
      &Rect,
      SDL_MapRGB(
        Surface->format,
        Color.r, Color.g, Color.b
      ));
  }

 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};
};
#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;
  };
}

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.
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
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

Get Started for Free
Implementing User Interaction
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

This course includes:

  • 118 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved