8
\$\begingroup\$

I am experimenting with ECS design and I am looking for a solid way to implement a message bus for use between the different systems. Here is a stripped-down version of my current implementation:

#include <iostream>
#include <cstdlib>
#include <functional>
#include <vector>

struct BaseEvent
{
    static size_t type_count;
    virtual ~BaseEvent() {}
};

size_t BaseEvent::type_count = 0;

template <typename EventType>
struct Event : BaseEvent
{
    static size_t type()
    {
        static size_t t_type = type_count++;
        return t_type;
    }
};


struct EventManager
{
    template <class EventType>
    using call_type = std::function<void(const EventType&)>;

    template <typename EventType>
    void subscribe(call_type<EventType> callable)
    {
        if (EventType::type() >= m_subscribers.size())
            m_subscribers.resize(EventType::type()+1);
        m_subscribers[EventType::type()].push_back(
            CallbackWrapper<EventType>(callable));
    }

    template <typename EventType>
    void emit(const EventType& event)
    {
        if (EventType::type() >= m_subscribers.size())
            m_subscribers.resize(EventType::type()+1);
        for (auto& receiver : m_subscribers[EventType::type()])
            receiver(event);

    }

    template <typename EventType>
    struct CallbackWrapper
    {
        CallbackWrapper(call_type<EventType> callable) : m_callable(callable) {}
        void operator() (const BaseEvent& event) { m_callable(static_cast<const EventType&>(event)); }
        call_type<EventType> m_callable;
    };

    std::vector<std::vector<call_type<BaseEvent>>> m_subscribers;
};

The classes are then used like so:

struct PLAYER_LVL_UP : Event<PLAYER_LVL_UP>
{ int new_level; };

struct PLAYER_HIT : Event<PLAYER_HIT>
{ int damage; };

struct COLLISION : Event<COLISSION>
{ Entity entity1; Entity entity2; };

struct PLAYER_GUI    
{

    PLAYER_GUI(EventManager& em, ...) : ...
    {
        using namespace std::placeholders;

        em.subscribe<PLAYER_HIT>(
            std::bind(&PLAYER_GUI::handle_hit, this, _1); 
        em.subscribe<PLAYER_LVL_UP>(
            std::bind(&PLAYER_GUI::handle_lvl_up, this, _1);
        .
        .
    }

    void handle_hit(const PLAYER_HIT& event)
    {
         // change rendering of life/player in the gui
    }

    void handle_lvl_up(const PLAYER_LVL_UP& event)
    {
        // change rendering of the lvl in the gui
    }

    ...
};


struct CollisionSystem : public System<CollisionSystem>
{
     .
     .
     void update(EventManager& em, float dt)
     {
         .
         .
         if (collides(entity1, entity2))
             em.emit(COLLISION{entity1, entity2});
     }
};

Obviously, this code snippet lacks a lot of things but at the moment I am mostly concerned with the approach taken. Specifically, I am not particularly sure about using static functions to map the event types to integers. I would really appreciate some general guidance in the right direction!

\$\endgroup\$
0

1 Answer 1

7
\$\begingroup\$

My suggestions:

Move type_count from being a public member

type_count plays an important role in the event manager. Making such a crucial part of the event management system a publically accessible member variable seems risky to me.

I would make that accessible only as a protected member function.

struct BaseEvent
{
   virtual ~BaseEvent() {}
   protected:
      static size_t getNextType();
};

size_t BaseEvent::getNextType()
{
   static size_t type_count = 0;
   return type_count++;
}

Of course, change Event appropriately.

template <typename EventType>
struct Event : BaseEvent
{
   static size_t type()
   {
      static size_t t_type = BaseEvent::getNextType();
      return t_type;
   } //; You don't need this semi-colon. Remove it.
};

Change the implementation of EventManager to simply event classes

Instead of

struct PLAYER_LVL_UP : Event<PLAYER_LVL_UP>
{ int new_level; };

struct PLAYER_HIT : Event<PLAYER_HIT>
{ int damage; };

struct COLLISION : Event<COLLISION>
{ Entity entity1; Entity entity2; };

it's cleaner to have:

struct PLAYER_LVL_UP
{ int new_level; };

struct PLAYER_HIT
{ int damage; };

struct COLLISION
{ Entity entity1; Entity entity2; };

You can accomplish that by updating EventManager to:

struct EventManager
{
   template <class EventType>
      using call_type = std::function<void(const EventType&)>;

   template <typename EventType>
      void subscribe(call_type<EventType> callable)
      {
         // When events such as COLLISION don't derive
         // from Event, you have to get the type by 
         // using one more level of indirection.
         size_t type = Event<EventType>::type();
         if (type >= m_subscribers.size())
            m_subscribers.resize(type+1);
         m_subscribers[type].push_back(CallbackWrapper<EventType>(callable));
      }

   template <typename EventType>
      void emit(const EventType& event)
      {
         // Same change to get the type.
         size_t type = Event<EventType>::type();
         if (type >= m_subscribers.size())
            m_subscribers.resize(type+1);

         // This a crucial change to the code.
         // You construct a temporary Event object by
         // using the EventType object and use Event.
         // This requires a change to Event, which follows below.
         Event<EventType> eventWrapper(event);
         for (auto& receiver : m_subscribers[type])
            receiver(eventWrapper);

      }

   template <typename EventType>
      struct CallbackWrapper
      {
         CallbackWrapper(call_type<EventType> callable) : m_callable(callable) {}

         void operator() (const BaseEvent& event) { 
          // The event handling code requires a small change too.
          // A reference to the EventType object is stored 
          // in Event. You get the EventType reference from the
          // Event and make the final call.
          m_callable(static_cast<const Event<EventType>&>(event).event_); }

         call_type<EventType> m_callable;
      };

   std::vector<std::vector<call_type<BaseEvent>>> m_subscribers;
};

The updated Event class:

template <typename EventType>
struct Event : BaseEvent
{
   static size_t type()
   {
      static size_t t_type = BaseEvent::getNextType();
      return t_type;
   }
   Event(const EventType& event) : event_(event) {}
   const EventType& event_;
};
\$\endgroup\$
10
  • \$\begingroup\$ Thanks for the answer! I was hoping for a review on the approach since this is just a code snippet I wrote on ideone.com and so I ignored naming and specifiers. In any case, I really like how you changed the Events! If I am not wrong, I will also have to differentiate between the event types so that they store copies instead of references if I want to store the events. \$\endgroup\$
    – Veritas
    Commented Feb 2, 2015 at 17:44
  • \$\begingroup\$ You had asked earlier about moving Event inside EventManager. I think that makes sense since both BaseEvent and Event have become implementation details of EventManager. As for storing copies of events types in Event, I don't see the need for it with the current design. \$\endgroup\$
    – R Sahu
    Commented Feb 2, 2015 at 18:57
  • \$\begingroup\$ There is no need with the current design but if I decide that I want to store events and emit them all at once, I will have to make sure that I won't have problems with invalid references to local objects. \$\endgroup\$
    – Veritas
    Commented Feb 2, 2015 at 19:02
  • \$\begingroup\$ Of course, that's always an option. \$\endgroup\$
    – R Sahu
    Commented Feb 2, 2015 at 19:10
  • 1
    \$\begingroup\$ The code is quite nice, and I have created a github gist (gist.github.com/asmwarrior/473a4ad40bfba7a1e8eb777ed0bb7846) to run like a demo program. Thanks. I come from this stackoverflow link: stackoverflow.com/questions/47485745/… \$\endgroup\$
    – ollydbg23
    Commented Jul 28, 2022 at 6:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.