This post details how I'm implementing general message passing in my engine. There's nothing particularly clever about how I'm doing it, but it seemed like a nice self-contained thing to write about.
Just one example of where such a thing is useful is when an entity's position changes; the grid, which is the data structure I'm using to partition the game world, must be updated with the entity's new position. I didn't want the Entity class to have to know about the Grid class because an entity is clearly a lower-level construct. Also, other classes/modules might be interested in 'entityMoved' events (or any other kind of event). This of course is the usual reason for such a message passing system -- to minimise dependencies between modules, and to eradicate "spaghetti code".
The class that implements this system is called EventManager, and the code listing is as follows.
#ifndef __EVENT_MANAGER_HPP__ #define __EVENT_MANAGER_HPP__ #include <queue> #include <map> #include "../utils/Functor.hpp" #include "EEvent.hpp" namespace Dodge { class EventManager { public: void immediateDispatch(EEvent* event); inline void queueEvent(EEvent* event); inline void registerCallback(long type, const Functor<void, TYPELIST_1(EEvent*)>& func); void unregisterCallback(long type, const Functor<void, TYPELIST_1(EEvent*)>& func); void doEvents(); void clear(); private: static std::map<long, std::vector<Functor<void, TYPELIST_1(EEvent*)> > > m_callbacks; static std::queue<EEvent*> m_eventQueue; }; //=========================================== // EventManager::queueEvent //=========================================== inline void EventManager::queueEvent(EEvent* event) { m_eventQueue.push(event); } //=========================================== // EventManager::registerCallback //=========================================== inline void EventManager::registerCallback(long type, const Functor<void, TYPELIST_1(EEvent*)>& func) { m_callbacks[type].push_back(func); } } #endif /*!__EVENT_MANAGER_HPP__*/
#include <definitions.hpp> #include <EventManager.hpp> using namespace std; namespace Dodge { map<long, vector<Functor<void, TYPELIST_1(EEvent*)> > > EventManager::m_callbacks; queue<EEvent*> EventManager::m_eventQueue; //=========================================== // EventManager::doEvents //=========================================== void EventManager::doEvents() { while (!m_eventQueue.empty()) { auto it = m_callbacks.find(m_eventQueue.front()->getType()); if (it != m_callbacks.end()) { vector<Functor<void, TYPELIST_1(EEvent*)> >& funcs = it->second; for (uint_t i = 0; i < funcs.size(); ++i) { funcs[i](m_eventQueue.front()); if (m_eventQueue.empty()) return; // In case funcs[i] calls EventManager::clear() } } delete m_eventQueue.front(); m_eventQueue.pop(); } EEvent::m_stack.clear(); } //=========================================== // EventManager::immediateDispatch //=========================================== void EventManager::immediateDispatch(EEvent* event) { auto it = m_callbacks.find(event->getType()); if (it != m_callbacks.end()) { vector<Functor<void, TYPELIST_1(EEvent*)> >& funcs = it->second; for (uint_t i = 0; i < funcs.size(); ++i) funcs[i](event); } delete event; } //=========================================== // EventManager::clear //=========================================== void EventManager::clear() { while (!m_eventQueue.empty()) m_eventQueue.pop(); EEvent::m_stack.clear(); } //=========================================== // EventManager::unregisterCallback //=========================================== void EventManager::unregisterCallback(long type, const Functor<void, TYPELIST_1(EEvent*)>& func) { auto it = m_callbacks.find(type); if (it != m_callbacks.end()) { for (uint_t i = 0; i < it->second.size(); ++i) { if (it->second[i] == func) { it->second.erase(it->second.begin() + i); --i; } } } } }
It's rather crude and simple as you can see, but it does the job.
EEvent is a base class from which other event types may be derived. As you can imagine though, spawning thousands of these objects every frame will likely be very expensive -- particularly if they're stored on the heap using malloc or operator new, requiring a context switch to and from kernel mode. For this reason I'm using a stack-based memory allocator.
#ifndef __EEVENT_HPP__ #define __EEVENT_HPP__ #include "definitions.hpp" #include "StackAllocator.hpp" namespace Dodge { class EEvent { friend class EventManager; public: static const int STACK_SIZE = 102400; // 100KB EEvent(long type) : m_type(type) {} inline long getType() const; static void* operator new(size_t size); static void operator delete(void* obj, size_t size); virtual ~EEvent() {}; private: long m_type; static StackAllocator m_stack; }; //=========================================== // EEvent::getType //=========================================== inline long EEvent::getType() const { return m_type; } } #endif /*!__EEVENT_HPP__*/
#include <Exception.hpp> #include <EEvent.hpp> using namespace std; namespace Dodge { StackAllocator EEvent::m_stack = StackAllocator(EEvent::STACK_SIZE); //=========================================== // EEvent::operator new //=========================================== void* EEvent::operator new(size_t size) { if (size == 0) size = 1; while (true) { #ifdef DEFAULT_NEW void* p; #else void* p = m_stack.alloc(size); if (p) return p; #endif p = ::operator new(size); if (p) return p; new_handler globalHandler = set_new_handler(0); set_new_handler(globalHandler); if (globalHandler) (*globalHandler)(); else throw std::bad_alloc(); } } //=========================================== // EEvent::operator delete //=========================================== void EEvent::operator delete(void* obj, size_t size) { #ifdef DEFAULT_NEW ::operator delete(obj); #endif } }
The stack is 'cleared' every frame by EventManager::doEvents(), so the lifetime of each EEvent instance must not exceed one frame.
The reason for DEFAULT_NEW is so that I can easily compare the event manager's performance when using the memory stack to when using the global operator new. It's used for benchmarking only, and is removed from the actual version.
Here is the code for the StackAllocator class:
#ifndef __STACK_ALLOCATOR_HPP__ #define __STACK_ALLOCATOR_HPP__ #include <cstring> #include "definitions.hpp" namespace Dodge { class StackAllocator { public: typedef unsigned int marker_t; StackAllocator(size_t size); ~StackAllocator(); void* alloc(size_t size); marker_t getMarker() const; void freeToMarker(marker_t marker); void clear(); private: size_t m_size; byte_t* m_top; byte_t* m_bottom; }; } #endif /*__STACK_ALLOCATOR_HPP__*/
#include <cstdlib> #include <StackAllocator.hpp> namespace Dodge { //=========================================== // StackAllocator::StackAllocator //=========================================== StackAllocator::StackAllocator(size_t size) : m_size(size) { m_bottom = m_top = new byte_t[size]; } //=========================================== // StackAllocator::~StackAllocator //=========================================== StackAllocator::~StackAllocator() { delete[] m_bottom; } //=========================================== // StackAllocator::alloc //=========================================== void* StackAllocator::alloc(size_t size) { byte_t* p = m_top; if (m_top + size > m_bottom + m_size) return NULL; m_top += size; return static_cast<void*>(p); } //=========================================== // StackAllocator::freeToMarker //=========================================== void StackAllocator::freeToMarker(marker_t marker) { m_top = m_bottom + marker; } //=========================================== // StackAllocator::getMarker //=========================================== StackAllocator::marker_t StackAllocator::getMarker() const { return m_top - m_bottom; } //=========================================== // StackAllocator::clear //=========================================== void StackAllocator::clear() { m_top = m_bottom; } }
Finally, to benchmark this message passing system, I used the following code:
#include <iostream> #include "EventManager.hpp" #include "Timer.hpp" using namespace Dodge; long num = 0; void eventHandler0(EEvent* event) { // Some random nonsense long i = (num / 10000) + 1; num += i; } void eventHandler1(EEvent* event) { // Some random nonsense long i = -(num / 10000) + 1; num += i; } int main() { EventManager eventManager; eventManager.registerCallback(0, Functor<void, TYPELIST_1(EEvent*)>(eventHandler0)); eventManager.registerCallback(1, Functor<void, TYPELIST_1(EEvent*)>(eventHandler1)); Timer timer; int i = 0; while (i < 10000) { for (int j = 0; j < 10000; ++j) { EEvent* event = new EEvent(j % 2); eventManager.queueEvent(event); } eventManager.doEvents(); ++i; } std::cout << "Time elapsed: " << timer.getTime() << " seconds\n"; return 0; }
The results were as follows (average of 3):
| DEFAULT_NEW defined | Running Time (seconds) |
| yes | 7.92 |
| no | 1.97 |
So in this case, I get a speed-up of a factor of four. On some systems, dynamic memory allocation is very expensive and so the savings might be even greater.