Since writing your own C++ game engine seems to be really popular these days (seriously just look at the amount of people presenting their WIPs on YouTube) I figured I'd try it myself.
My mental model of an event system looks like this:
Events
are basically signals that tell you that something has happened. Certain types of events may also hold additional information about some state in the form of member variables. However events do not act. They are just information that is passed around.- All classes that want to partake in the event system need to implement an
EventHandler
interface. EventHandlers
are responsible for dispatching, receiving/storing and processing events.- Each instance of an
EventHandler
holds a list of references to otherEventHandlers
. These other handlers receive it's broadcasted events. - When a handler receives an event it stores the event in a queue, so processing can be scheduled.
- Each implementation of the
EventHandler
interface react differently to events. Different types of events may need to be addressed differently.
- Each instance of an
- The "user" of the engine is free to define all types of
Events
andEventHandlers
(i.e. implementations of them).
Here is my current approach that "works" (I am positive that it's horrible, since the user has to trial-and-error dynamic_cast
the event):
- The "engine" side of the event system:
/**
* Engine code
*/
// Event.hpp/cpp
class IEvent
{
/* Event interface */
protected:
virtual ~IEvent() = default;
};
// EventHandler.hpp/cpp
class IEventHandler
{
public:
// Send events to other handlers
void dispatch_event(const IEvent* event)
{
for (auto& recipient : event_recipients)
{
recipient->event_queue.push(event);
}
}
// Invoke processing for events in queue when the time has come (oversimplified)
void process_event_queue()
{
while (!event_queue.empty())
{
event_callback(event_queue.front());
event_queue.pop();
}
}
// Push to queue manually
void push_queue(const IEvent* event)
{
event_queue.push(event);
}
protected:
// Store events so their processing can be scheduled
std::queue<const IEvent*> event_queue;
// Who will receive event dispatches from this handler
std::set<IEventHandler*> event_recipients;
// Process each individual event
virtual void event_callback(const IEvent* event) = 0;
};
- How the "user" might typically interact with it:
/**
* "User" code
*/
// UserEvents.hpp/cpp
class UserEventA : public IEvent {};
class UserEventB : public IEvent {};
class UserEventC : public IEvent {};
// UserEventHandler.hpp/cpp
class UserEventHandler : public IEventHandler
{
protected:
// AFAIK this is painfully slow
void event_callback(const IEvent* event) override
{
if (auto cast_event = dynamic_cast<const UserEventA*>(event))
{
cout << "A event" << endl;
}
else if (auto cast_event = dynamic_cast<const UserEventB*>(event))
{
cout << "B event" << endl;
}
else
{
cout << "Unknown event" << endl;
}
}
};
int main()
{
// Create instances of user defined events
UserEventA a;
UserEventB b;
UserEventC c;
// Instance of user defined handler
UserEventHandler handler;
// Push events into handlers event queue
handler.push_queue(&a);
handler.push_queue(&b);
handler.push_queue(&c);
// Process events
handler.process_event_queue();
}
Some alternatives that I've already explored but didn't lead me anywhere:
- The visitor pattern (utilizing double dispatch) seems like a good idea but only accounts for the "visitables" to be extendable. IIRC the "visitors" usually have a rigidly defined interface. Here however both the
Events
and theEventHandlers
are subject to change and thus I don't think the visitor pattern can be applied. - Replacing the
IEvent*
in theevent_queue
of theEventHandler
interface withstd::variant
would enable me to use the comparatively faststd::get_if
instead of costlydynamic_casts
. Every implementation would know what event types it can process. However this would make dispatching events between different implementations that accept different event types impossible, due to their variants (and thus their queues) being structured differently.
2 Answers 2
Why?
What's it actually for? What sort of "event" are we dealing with, and why do we need to delay dealing with that event, instead of just calling a function directly?
Why do we need event type erasure, instead of dealing with specific event types, e.g. IEventHandler<T>
implementing void event_callback(T const& e)
?
This is all quite abstract. So it's hard to tell if dispatching events like this is appropriate instead of something more like delegates or signals.
(I'm not saying the design is necessarily invalid, but we'd need some concrete examples of what it's actually being used for in a game).
Separate dispatch and handling
I think it's more usual to separate the dispatching of events and the receiving of events.
Right now a class that only needs to dispatch events has an unnecessary queue of events to process, and a function to process them.
A class that only needs to receive events also has an unnecessary list of recipients.
So a separate IEventHandler
and IEventDispatcher
would probably be a good idea.
Interface and access control
std::queue<const IEvent*> event_queue;
std::set<IEventHandler*> event_recipients;
Making these protected
is a little dangerous. It would be better for the base class to implement a more complete interface (e.g. dispatcher.add_recipient(&foo_object);
), and then make these variables private
.
Too many queues
Note that giving each event recipient its own event queue might not be a good idea. With a 100 listeners to an event (not unreasonable, depending on what this is used for), dispatching an event involves pushing it to 100 different queues.
It might be better to keep the event queue on the dispatch side, and have the dispatcher call a process_event
function on each recipient instead.
-
\$\begingroup\$ I'll try to address your points one by one: 1. Why?: As I said, these events act as general purpose notifications for engine or game components to react to. They can range from something as general as keyboard input or the application window closing to something as specific as a weapon being fired in a game. I do not know which
EventHandler
would want to react to which and how many types of events. The reason I'm buffering the events instead of processing them directly is because in order to keep performance of my game engine steady, processes might need to be scheduled. \$\endgroup\$TheBeautifulOrc– TheBeautifulOrc2021年04月09日 17:36:26 +00:00Commented Apr 9, 2021 at 17:36 -
\$\begingroup\$ 2. Interface and access controll: You're absolutely right. 3. Separate dispatch/handling & too many queues: The approach you're suggesting is quite interesting and I'll reconsider it down the road. However one "dispatcher queue" might make the entire idea of scheduling event processing difficult and does not solve the problem I'm currently facing. Also the optimization of reducing the number of queues (mind that they only hold pointers to my events, there is no costly copying here) seems really low-level and is propably not the most pressing issue with my current code. \$\endgroup\$TheBeautifulOrc– TheBeautifulOrc2021年04月09日 17:43:21 +00:00Commented Apr 9, 2021 at 17:43
-
2\$\begingroup\$ Nice answer, but I’m surprised no-one is commenting on the lifetime management issues. It seems a little barmy that the event queue doesn’t take ownership of the event object, like, via a smart pointer. So if I want to make an event, I have to create the event object... and hold onto it for an indefinite amount of time until the event handler gets around to processing it, which I’ll somehow have to find out about (how?), and only then delete it? (The example code given, where the events have to outlive the event handler to hide this problem, is absurd.) \$\endgroup\$indi– indi2021年04月10日 04:00:14 +00:00Commented Apr 10, 2021 at 4:00
-
\$\begingroup\$ @indi Ouch, that one was obvious... Thanks for the reminder! \$\endgroup\$TheBeautifulOrc– TheBeautifulOrc2021年04月10日 15:57:55 +00:00Commented Apr 10, 2021 at 15:57
It is not a healthy idea for a game engine to deal this way with general events.
The class IEvent
is not particularly useful. Dynamic cast operation is a rather heavy operation and it isn't healthy to use it for something as basic as mouse click or keyboard click.
Just think of it. You'll have hundreds of events and hundreds of potential clients for the events and each will have to perform a bunch of dynamic casts to even figure out if the event is even relevant. And probably half the time they will do the same casts over and over again.
Consider trying the data oriented design instead of object oriented design. Here is a link from cppcon explaining it vs OOP
https://www.youtube.com/watch?v=yy8jQgmhbAU&ab_channel=CppCon
I also answer to some comments from another answer:
- Why?: As I said, these events act as general purpose notifications for engine or game components to react to. They can range from something as general as keyboard input or the application window closing to something as specific as a weapon being fired in a game. I do not know which EventHandler would want to react to which and how many types of events. The reason I'm buffering the events instead of processing them directly is because in order to keep performance of my game engine steady, processes might need to be scheduled.
I believe a more healthy approach is a subscription model. Where certain event handlers subscribe to certain types of events. For instance, you classify events in several broad categories and let event handlers listen only to events of certain general categories they are interested it.
Second, let event handlers decide whether they want to process the event immediately or push into a processing queue. If dealing with the event is quick enough there might be no need in scheduling it to a later time at all. For example, to handle it might simply performing more finetuned tests on the event and then deciding when to schedule it and where or perhaps drop it completely - since the initial categorization is probably rather broad you might need additional filtering.
Explore related questions
See similar questions with these tags.
dynamic_casts
in order to determine the event type, which is probably not suited for a performance crirtical real-time application like a game engine. \$\endgroup\$QEvent::Type
) that you have to adhere to. This approach seems unsuitable for a game engine where the game designer might come up with the most absurd and specific event types. \$\endgroup\$