The previous lesson covered how we could use an event loop to handle keyboard and mouse events from our users.
Having set up an application and event loop to handle user input, the next step is to build our next set of systems on top of it.
Managing all events directly in the event loop can get quite messy, so we should come up with a better way of managing it. In this lesson, we’ll establish a simple UI system, to handle all the complexity
The design pattern of event layers can be useful here.
Fundamentally, our goal is to transform an input event into some change in our application state. The change we need to make is based on the user intent that triggered that event.
In a game, for example, we might be rendering a 3D world, and have a series of UI elements layered on top.
If a user clicks their mouse, they might be trying to interact with the 3D world, like firing their weapon in a shooter game, or selecting a unit in a strategy game.
But, if the user was hovering over a button when they clicked their mouse, they were interacting with that button instead.
The input event hasn’t changed, but the user intent is completely different.
Out of this, the concept of an event layer was created. We can think of the UI and the game world being two different layers. The UI layer sits on top of the world layer.
When a mouse event is received, the event loop first hands it to the top layer - the UI, in this case. The UI inspects the event and decides whether or not to handle it.
If the UI handles the event, the journey stops there - the lower layers never see it.
If the UI doesn’t handle the event, it then gets passed to the next layer, which will have the opportunity to handle it, and the process repeats.
Let's see a simple example. Note, Window.h
is the same file we created in our previous lesson on creating an SDL2Â window.
#include <SDL.h>
#include "Window.h"
class Layer {
public:
bool HandleEvent(const SDL_Event* Event) {
// Return true if the event was handled
// Return false otherwise
return false;
}
};
int main() {
Window GameWindow;
Layer UI;
Layer World;
SDL_Event Event;
while(true) {
while(SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
if (UI.HandleEvent(&Event)) {
// The UI took care of this event
// Continue to the next
continue;
}
// The UI did not handle this event
// Let the next layer see it
if (World.HandleEvent(&Event)) {
continue;
}
}
}
}
First, we need a way for objects to register themselves with an event layer. To do that, we’ll create a new class: EventReceiver
. Similar to the layers, they’ll have a function to receive events, and that function will return a boolean representing whether or not they handled that event:
class EventReceiver {
public:
virtual bool HandleEvent(const SDL_Event* Event) {
return false;
}
};
Then, we’ll update our Layer
class with a way to let EventReceivers
subscribe to that layer’s events:
class Layer {
public:
bool HandleEvent(const SDL_Event* Event) {
// Return true if the event was handled
// Return false otherwise
return false;
}
void SubscribeToEvents(EventReceiver* Receiver) {
Subscribers.push_back(Receiver);
}
private:
std::vector<EventReceiver*> Subscribers;
};
Finally, the HandleEvent
function on our Layer
needs to notify all of its subscribers of the event. It will return true
if one of its subscribers handled it, and false
 otherwise:
class Layer {
public:
bool HandleEvent(const SDL_Event* Event) {
for (const auto Handler : Subscribers) {
if (Handler->HandleEvent(Event)) {
return true;
}
}
return false;
}
// ...
}
Lets see everything we’ve got so far:
#include <SDL.h>
#include <vector>
#include <iostream>
#include "Window.h"
class EventReceiver {
public:
virtual bool HandleEvent(const SDL_Event* Event) {
std::cout << "It's working!!\n";
return false;
}
};
class Layer {
public:
bool HandleEvent(const SDL_Event* Event) {
for (const auto Handler : Subscribers) {
if (Handler->HandleEvent(Event)) {
return true;
}
}
return false;
}
void SubscribeToEvents(EventReceiver* Receiver) {
Subscribers.push_back(Receiver);
}
private:
std::vector<EventReceiver*> Subscribers;
};
int main() {
Window GameWindow;
Layer UI;
Layer World;
EventReceiver ExampleButton;
UI.SubscribeToEvents(&ExampleButton);
SDL_Event Event;
while(true) {
while(SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
if (UI.HandleEvent(&Event)) {
// The UI took care of this event
// Continue to the next
continue;
}
// The UI did not handle this event
// Let the next layer see it
if (World.HandleEvent(&Event)) {
continue;
}
}
}
}
Running this application, we should see our window pop up, and our ExampleButton
receiving lots of events as we move our mouse around. Most importantly, our main event loop, and main
function in general, is simple and clean.
Our example here is quite basic, but the core principles are what is important. Some enhancements we could make include:
EventReceiver
that accepts a Layer
, then handling event subscription within the EventReceiver
class.EventReceivers
to subscribe to only certain types of events. For example, SubscribeToEvents
and Subscribers
could be separated out, allowing distinction between keyboard events, mouse click events, mouse motion eventsTypically, the architecture here will just evolve as our requirements develop.
In the next lesson, we’ll expand on this UI architecture to create buttons that can interact with our core application.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games