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!
1 Answer 1
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_;
};
-
\$\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– Veritas2015年02月02日 17:44:29 +00:00Commented Feb 2, 2015 at 17:44
-
\$\begingroup\$ You had asked earlier about moving
Event
insideEventManager
. I think that makes sense since bothBaseEvent
andEvent
have become implementation details ofEventManager
. As for storing copies of events types inEvent
, I don't see the need for it with the current design. \$\endgroup\$R Sahu– R Sahu2015年02月02日 18:57:19 +00:00Commented 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– Veritas2015年02月02日 19:02:32 +00:00Commented Feb 2, 2015 at 19:02
-
\$\begingroup\$ Of course, that's always an option. \$\endgroup\$R Sahu– R Sahu2015年02月02日 19:10:00 +00:00Commented 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– ollydbg232022年07月28日 06:04:04 +00:00Commented Jul 28, 2022 at 6:04
Explore related questions
See similar questions with these tags.