This is a single header event manager that I'm using in my game.
Disclaimer
I ripped everything from these lovely people:
so give me little to no credit for the ideas used, but I did write every single line myself (using they're implementations as heavy motivation).
Looking for
- if it's readable
- if there are any hidden bugs (I'm an amateur C++ hobbiest programmer, ~1 year, little experience with static and used it anyway)
- features I should add (does this cover everything I need to make a small scale game? I have little experience and don't know if I covered everything)
- just an overall review would be nice (:
The goals of the event manager
- no base event class
- events are just POD structs
- as few virtual methods as possible
- no explicit registering of events
- fast contiguous execution of functions (I dont really mind if subscribing takes longer to achieve this)
- simple api (no std::bind, lambdas, etc...)
- single header
Example usage:
#include <iostream> #include "EventManager.h" // events struct LevelDown { int level; }; struct LevelUp { int level; }; // event handlers / listeners / subscribers void handleLevelUp(const LevelUp& event) { std::cout << "level: " << event.level << '\n'; } void handleLevelDown(const LevelDown& event) { std::cout << "downlevel: " << event.level << '\n'; } void levelDownConsiquence(const LevelDown& event) { std::cout << "downlevel consiquence: " << event.level << '\n'; } class DownLevelClass { public: explicit DownLevelClass(EventManager* em) { em->subscribe<LevelDown>(&DownLevelClass::method, this); } private: void method(const LevelDown& event) const { std::cout << "method down level: " << event.level << '\n'; } }; int main() { EventManager em; int level{ 0 }; // use handle to unsubscibe auto levelUpHandle = em.subscribe<LevelUp>(&handleLevelUp); // it is not neccissary to have it if you plan on never unsubscribing em.subscribe<LevelDown>(&handleLevelDown); em.subscribe<LevelDown>(&levelDownConsiquence); DownLevelClass DLC(&em); level--; em.publishBlocking<LevelDown>({ level }); level++; em.publishBus<LevelUp>({ level }); em.publishBlocking<LevelUp>({ level }); em.pollEvents(); em.unsubscribe<LevelUp>(levelUpHandle); } This should output:
downlevel: -1 downlevel consiquence: -1 method down level: -1 level: 0 level: 0 Summary of the functionality provided
- you can subscribe with a POD struct type and a raw function pointer
- you can unsubscribe by passing in the handle returned by the subscribe and the event type
- you can publish blocking events with the event type and the same instance of that event type (has to be pod)
- you can publish to the bus (same rules as publishing a blocking event)
- you can poll bus events by calling poll events, this is blocking
- unsubscribing does exactly what it says, needs event type and handle from subscribe.
Implementation (EventManager.h)
#pragma once #include <vector> #include <functional> #include <memory> #include <unordered_map> #include <assert.h> class ICallbackContainer { public: virtual ~ICallbackContainer() = default; virtual void callSaved() const = 0; }; template<typename EventType> class CallbackContainer : public ICallbackContainer { public: using CallbackType = std::function<void(const EventType&)>; using SubscriberHandle = size_t; SubscriberHandle addCallback(CallbackType callback); void removeCallback(SubscriberHandle handle); void operator() (const EventType& event) const { for (auto& callback : m_Callbacks) { callback(event); } } void save(const EventType& event); void callSaved() const override; private: std::vector<CallbackType> m_Callbacks{}; std::vector<SubscriberHandle> m_FreeHandles{}; std::unordered_map<SubscriberHandle, size_t> m_HandleToIndex{}; std::unordered_map<size_t, SubscriberHandle> m_IndexToHandle{}; EventType m_SavedEvent{}; }; template<typename EventType> auto CallbackContainer<EventType>::addCallback(CallbackType callback) -> SubscriberHandle { SubscriberHandle handle; size_t newIndex = m_Callbacks.size(); if (m_FreeHandles.empty()) { handle = m_Callbacks.size(); } else { handle = m_FreeHandles.back(); m_FreeHandles.pop_back(); } m_HandleToIndex[handle] = newIndex; m_IndexToHandle[newIndex] = handle; if (newIndex >= m_Callbacks.size()) { m_Callbacks.resize(newIndex + 1); } m_Callbacks[newIndex] = callback; return handle; } template<typename EventType> void CallbackContainer<EventType>::removeCallback(SubscriberHandle handle) { assert(m_HandleToIndex.find(handle) != m_HandleToIndex.end()); size_t indexOfRemovedHandle = m_HandleToIndex[handle]; size_t indexOfLastElement = m_Callbacks.size() - 1; if (indexOfRemovedHandle != indexOfLastElement) { SubscriberHandle handleOfLastElement = m_IndexToHandle[indexOfLastElement]; m_HandleToIndex[handleOfLastElement] = indexOfRemovedHandle; m_IndexToHandle[indexOfRemovedHandle] = handleOfLastElement; m_Callbacks[indexOfRemovedHandle] = m_Callbacks[indexOfLastElement]; } else { m_Callbacks.pop_back(); } m_HandleToIndex.erase(handle); m_IndexToHandle.erase(indexOfLastElement); m_FreeHandles.emplace_back(handle); } template<typename EventType> void CallbackContainer<EventType>::save(const EventType& event) { m_SavedEvent = event; } template<typename EventType> void CallbackContainer<EventType>::callSaved() const { for (auto& callback : m_Callbacks) { callback(m_SavedEvent); } } class EventManager { public: template<typename EventType, typename Function> typename CallbackContainer<EventType>::SubscriberHandle subscribe(Function callback); template<typename EventType, typename Method, typename Instance> typename CallbackContainer<EventType>::SubscriberHandle subscribe(Method callback, Instance instance); template<typename EventType> void unsubscribe(typename CallbackContainer<EventType>::SubscriberHandle handle); template<typename EventType> void publishBlocking(const EventType& event) const; template<typename EventType> void publishBlocking(EventType&& event) const; template<typename EventType> void publishBus(const EventType& event); template<typename EventType> void publishBus(EventType&& event); void pollEvents(); private: template<typename EventType> static inline CallbackContainer<EventType> s_Callbacks; std::vector<const ICallbackContainer*> m_EventBus; }; template<typename EventType, typename Function> inline typename CallbackContainer<EventType>::SubscriberHandle EventManager::subscribe(Function callback) { return s_Callbacks<EventType>.addCallback(callback); } template<typename EventType, typename Method, typename Instance> typename CallbackContainer<EventType>::SubscriberHandle EventManager::subscribe(Method callback, Instance instance) { std::function<void(const EventType&)> function{ std::bind(callback, instance, std::placeholders::_1) }; return s_Callbacks<EventType>.addCallback(std::move(function)); } template<typename EventType> inline void EventManager::unsubscribe(typename CallbackContainer<EventType>::SubscriberHandle handle) { s_Callbacks<EventType>.removeCallback(handle); } template<typename EventType> inline void EventManager::publishBlocking(const EventType& event) const { s_Callbacks<EventType>(event); } template<typename EventType> inline void EventManager::publishBlocking(EventType&& event) const { s_Callbacks<EventType>(std::forward<EventType>(event)); } template<typename EventType> void EventManager::publishBus(const EventType& event) { s_Callbacks<EventType>.save(event); m_EventBus.emplace_back(&s_Callbacks<EventType>); } template<typename EventType> void EventManager::publishBus(EventType&& event) { s_Callbacks<EventType>.save(std::forward<EventType>(event)); m_EventBus.emplace_back(&s_Callbacks<EventType>); } inline void EventManager::pollEvents() { for (const auto& callback : m_EventBus) { callback->callSaved(); } m_EventBus.clear(); } Questions
- I'm worried the static
s_Callbacksin theEventManagerclass is bad - scaleability (in usage and with features)
- features I should add (in the context of OpenGL and windows.h gamedev)
- is there any way to get rid of the base
CallbackContainerclass? do I even need too?