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:
Eventsare 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
EventHandlerinterface. EventHandlersare responsible for dispatching, receiving/storing and processing events.- Each instance of an
EventHandlerholds 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
EventHandlerinterface 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
EventsandEventHandlers(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
Eventsand theEventHandlersare subject to change and thus I don't think the visitor pattern can be applied. - Replacing the
IEvent*in theevent_queueof theEventHandlerinterface withstd::variantwould enable me to use the comparatively faststd::get_ifinstead 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_castsin 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\$