HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

ECS Event/Messaging implementation

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
implementationeventmessagingecs

Problem

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 
#include 
#include 
#include 

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

size_t BaseEvent::type_count = 0;

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

struct EventManager
{
    template 
    using call_type = std::function;

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

    template 
    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 
    struct CallbackWrapper
    {
        CallbackWrapper(call_type callable) : m_callable(callable) {}
        void operator() (const BaseEvent& event) { m_callable(static_cast(event)); }
        call_type m_callable;
    };

    std::vector>> m_subscribers;
};


The classes are then used like so:

```
struct PLAYER_LVL_UP : Event
{ int new_level; };

struct PLAYER_HIT : Event
{ int damage; };

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

struct PLAYER_GUI
{

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

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

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

Solution

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 
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
{ int new_level; };

struct PLAYER_HIT : Event
{ int damage; };

struct COLLISION : Event
{ 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 
      using call_type = std::function;

   template 
      void subscribe(call_type 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::type();
         if (type >= m_subscribers.size())
            m_subscribers.resize(type+1);
         m_subscribers[type].push_back(CallbackWrapper(callable));
      }

   template 
      void emit(const EventType& event)
      {
         // Same change to get the type.
         size_t type = Event::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 eventWrapper(event);
         for (auto& receiver : m_subscribers[type])
            receiver(eventWrapper);

      }

   template 
      struct CallbackWrapper
      {
         CallbackWrapper(call_type 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&>(event).event_); }

         call_type m_callable;
      };

   std::vector>> m_subscribers;
};


The updated Event class:

template 
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_;
};

Code Snippets

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

size_t BaseEvent::getNextType()
{
   static size_t type_count = 0;
   return type_count++;
}
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.
};
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; };
struct PLAYER_LVL_UP
{ int new_level; };

struct PLAYER_HIT
{ int damage; };

struct COLLISION
{ Entity entity1; Entity entity2; };
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;
};

Context

StackExchange Code Review Q#79211, answer score: 7

Revisions (0)

No revisions yet.