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 other EventHandlers. 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.
- The "user" of the engine is free to define all types of
Events and EventHandlers (i.e. implementations of them).
Here is my current approach that "works" (I am positive that it's horrible):
- 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:
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 the EventHandlers are subject to change and thus I don't think the visitor pattern can be applied.
- Replacing the
IEvent* in the event_queue of the EventHandler interface with std::variant would enable me to use the comparatively fast std::get_if instead of costly dynamic_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.