Introduction
I have just started learning templates and experimenting with function pointers. I wanted to create an event system that met a couple of goals:
- Event types are PODs, and do not inherit from a base 'Event' class
- External API is simple, no binding or passing in lambdas etc.
- No explicit registering of event types by user, any POD struct like object should work
The implementation currently works, however hasn't been heavily tested. The code will be used in my small "game engine" project, done purely as a learning exercise.
Focus
I am most interested in the way I am storing and calling the functions. I wanted a way to store any arbitrary event type that met the conditions described above.
I'm not convinced on the way I am handling the ListenerHandle
s which are solely used to enable the remove_listener
feature. However this is of less concern to me than my previous point.
Future
- Multithreading/Async
- Delayed event queues
Usage
I'll start with what it looks like to use the API, I am relatively happy with this aspect:
#include "EventManager.h"
const int key_w = 87;
struct CollisionEvent
{
int x;
int y;
};
struct KeyPressEvent
{
int key_code;
};
class InputHandler
{
public:
InputHandler()
{
EventManager::listen<KeyPressEvent>(this, &InputHandler::on_key_press);
}
private:
void on_key_press(KeyPressEvent* event)
{
// ...
}
};
void handle_collision(CollisionEvent* event)
{
// ...
}
int main()
{
InputHandler input_handler;
EventManager::fire<KeyPressEvent>(key_w);
ListenerHandle handle = EventManager::listen<CollisionEvent>(&handle_collision);
EventManager::fire<CollisionEvent>(10, 3);
EventManager::remove_listener(handle);
}
Implementation
It is implemented in a single header EventManager.h
, and is roughly ~140 lines without whitespace. I will divide each part into its own block for readability.
ListenerHandle
A ListenerHandle
is returned to the user upon registering an event callback. Each callback function is uniquely associated with one handle, allowing users to remove their callback.
#pragma once
#include <cassert>
#include <cstdint>
#include <vector>
#include <memory>
#include <functional>
#include <queue>
typedef std::uint32_t EventID;
class ListenerHandle
{
public:
ListenerHandle(std::uint32_t sparse_index = 0u, std::uint32_t dense_index = 0u, std::uint32_t version = 0u, EventID event_id = 0u)
: m_sparse_index(sparse_index),
m_dense_index(dense_index),
m_version(version),
m_event_id(event_id) {}
public:
std::uint32_t m_sparse_index;
std::uint32_t m_dense_index;
std::uint32_t m_version;
EventID m_event_id;
friend class EventManager;
template<typename T>
friend struct CallbackContainer;
};
CallbackContainer
A callback container is used to store each callback of the same event type (i.e from the example above, all KeyPressEvent
callbacks would be stored together in an instance of CallbackContainer
).
A sparse set is used to track which ListenerHandle
belongs to which callback. Each index in the sparse-set is versioned, so you can only remove the callback originally associated with your handle.
struct CallbackContainerBase
{
virtual void remove_callback(const ListenerHandle& handle) = 0;
};
template<typename T>
struct CallbackContainer : CallbackContainerBase
{
CallbackContainer(EventID event_id) : event_id{ event_id } {}
std::vector<std::function<void(T*)>> callbacks;
std::vector<ListenerHandle> handles;
EventID event_id;
std::vector<ListenerHandle> sparse;
std::vector<std::uint32_t> dense;
std::queue<std::uint32_t> free_sparse_indices;
template<typename T_Function>
auto add_callback(T_Function callback)->ListenerHandle;
void remove_callback(const ListenerHandle& handle) override;
};
template<typename T>
inline void CallbackContainer<T>::remove_callback(const ListenerHandle& handle)
{
assert(handle.m_version == sparse[handle.m_sparse_index].m_version);
sparse[handle.m_sparse_index].m_version++;
std::uint32_t remove_index = sparse[handle.m_sparse_index].m_dense_index;
std::uint32_t last_index = callbacks.size() - 1;
dense[remove_index] = dense[last_index];
sparse[dense[remove_index]].m_dense_index = remove_index;
std::swap(callbacks[remove_index], callbacks[last_index]);
free_sparse_indices.push(handle.m_sparse_index);
callbacks.pop_back();
dense.pop_back();
}
template<typename T>
template<typename T_Function>
inline auto CallbackContainer<T>::add_callback(T_Function callback) -> ListenerHandle
{
std::uint32_t sparse_index;
if (free_sparse_indices.empty())
{
sparse_index = callbacks.size();
sparse.emplace_back(sparse_index, sparse_index, 0u, event_id);
}
else
{
sparse_index = free_sparse_indices.front();
free_sparse_indices.pop();
}
dense.push_back(sparse_index);
callbacks.emplace_back(callback);
return sparse[sparse_index];
}
EventManager
This is the class that users interact with, a trivial system is used to create run-time available IDs to identify event types, which act as indexes into the vector of CallbackContainer
's.
Each CallbackContainer
is stored as its base CallbackContainerBase
to allow for the storage of any arbitrary type, and then casted to the correct type when needed.
class EventManager
{
using CallbackContainers = std::vector<std::unique_ptr<CallbackContainerBase>>;
public:
template<typename T, typename T_Function>
static auto listen(T_Function callback)->ListenerHandle;
template<typename T, typename T_Instance, typename T_Function>
static auto listen(T_Instance* instance, T_Function callback)->ListenerHandle;
static void remove_listener(const ListenerHandle& handle);
template<typename T, typename... T_Args>
static void fire(T_Args...args);
private:
template<typename T>
static auto get_event_id()->EventID;
template<typename T>
static auto register_event()->EventID;
private:
static inline CallbackContainers s_callbacks;
static inline EventID s_next_event_id{ 0u };
};
template<typename T, typename T_Function>
inline ListenerHandle EventManager::listen(T_Function callback)
{
return static_cast<CallbackContainer<T>*>(s_callbacks[get_event_id<T>()].get())->add_callback(callback);
}
template<typename T, typename T_Instance, typename T_Function>
inline ListenerHandle EventManager::listen(T_Instance* instance, T_Function callback)
{
return static_cast<CallbackContainer<T>*>(s_callbacks[get_event_id<T>()].get())->add_callback([instance, callback](T* event) { (instance->*callback)(event); });
}
inline void EventManager::remove_listener(const ListenerHandle& handle)
{
s_callbacks[handle.m_event_id]->remove_callback(handle);
}
template<typename T, typename ...T_Args>
inline void EventManager::fire(T_Args ...args)
{
T event{ args... };
auto& callbacks = static_cast<CallbackContainer<T>*>(s_callbacks[get_event_id<T>()].get())->callbacks;
for (auto& callback : callbacks)
callback(&event);
}
template<typename T>
inline EventID EventManager::get_event_id()
{
static EventID event_id = register_event<T>();
return event_id;
}
template<typename T>
inline EventID EventManager::register_event()
{
s_callbacks.emplace_back(std::make_unique<CallbackContainer<T*>>(s_next_event_id));
return s_next_event_id++;
}
Thank you for your time, it is greatly appreciated!