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.
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;
};
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.
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.
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
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)};
}
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:
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.
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 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 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};
// ...
};
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.
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
this
in the User EventA 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.
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;
};
}
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.SDL_Event
union's user member (SDL_UserEvent
) holds custom data.user.data1
and user.data2
are void pointers for arbitrary data.user.data1
and user.data2
requires careful type casting (static_cast
) and lifetime management.Uint32
) in a shared header (e.g., UserEvents.h
) improves organization.Learn how to create and manage your own game-specific events using SDL's event system.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games