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_Callbacks
in theEventManager
class 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
CallbackContainer
class? do I even need too?