Skip to main content
added 99 characters in body
Source Link

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):

/**
 * "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();
}

Here is my current approach that "works" (I am positive that it's horrible):

/**
 * "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();
}

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):

/**
 * "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();
}
Source Link

C++: Event system for game engine

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):

  1. 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;
};
  1. 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:

  1. 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.
  2. 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.