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.
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\$