patterncppMinor
ECS Event/Messaging implementation
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:
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
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
I would make that accessible only as a
Of course, change
Change the implementation of
Instead of
it's cleaner to have:
You can accomplish that by updating
The updated
Move
type_count from being a public membertype_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 classesInstead 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.