6
\$\begingroup\$

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 the EventManager 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?
\$\endgroup\$
0

1 Answer 1

4
\$\begingroup\$

Answers to your questions

  • if it's readable

I think the code is quite readable. I would add documentation though using Doyxgen.

  • if there are any hidden bugs (I'm an amateur C++ hobbiest programmer, ~1 year, little experience with static and used it anyway)

If you call publishBus() twice with the same event type, then the old saved event is overwritten, but now callSaved() will be called twice on the new event.

  • 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)

This is impossible to answer in general. There are many games that don't need an event manager to begin with. And if you do need an event manager, maybe this one suffices, even though a different one might be better. If you are working on a game, you know best yourself whether this event manager is what you need.

Even better than adding features would be to see which features you can remove.

  • I'm worried the static s_Callbacks in the EventManager class is bad

What would be bad about this? It looks fine to me.

  • scaleability (in usage and with features)

I don't see any scalability issues when it comes to performance or memory usage. I'm not sure what you mean with "with features", unless:

  • features I should add (in the context of OpenGL and windows.h gamedev)

You should not add features unless necessary. Your event manager has a simple job: handle events and forward them to subscribers. Instead of making it do more different things, focus on making sure it does those few things well.

  • is there any way to get rid of the base CallbackContainer class? do I even need too?

You don't need to, but it is possible. You might use std::variant and std::visit(), but then you need to know the full set of event types up front. You could also do some form of type erasure. For example, instead of m_EventBus storing pointers to ICallbackContainer, have it store std::function<void()>s:

class EventManager {
    …
    std::vector<std::function<void()>> m_EventBus;
};

template<typename EventType>
void EventManager::publishBus(const EventType& event) {
    s_Callbacks<EventType>.save(event);
    m_EventBus.emplace_back([&]{ s_Callbacks<EventType>.callSaved(); });
}

void EventManager::pollEvents() {
    for (auto& callback : m_EventBus) {
        callback();
    }
    m_EventBus.clear();
}

This has roughly the same overhead as your solution using inheritance.

Why have both publishBlocking() and publishBus()?

I don't see why you have both publishBlocking() and publishBus(). Why do you need to store an event and call the subscribers later? If you really need that, then why only have the ability to store one event? I would expect a queue then where you can publish many events, and have them all polled together later.

If you do not really need both, then you should follow the YAGNI principle.

Mapping indices to handles

You have opted to store a dense vector of callbacks. This is great if you call the callbacks much more often than you modify the set of callbacks, which is a reasonable assumption. However, now you need to map indices to handles in order to support \$O(1)\$ insertion and removal of callbacks. Using std::unordered_map is one way of doing that, but it has some overhead. You might want to use std::vectors for m_HandleToIndex and m_IndexToHandle:

  • std::vector<SubscriberHandle> m_IndexToHandle is trivial: its size is the same as m_Callbacks, and you just store the handle for a given index at that same index.
  • std::vector<size_t> m_HandleToIndex is now a potentially sparse vector, indexed by SubscriberHandle. It might seem bigger than necessary, but it has much less overhead than std::unordered_map, so it will still use less memory unless it has become really sparse.

Accessing those std::vectors will be faster than std::unordered_maps, even if both are \$O(1)\$.

\$\endgroup\$
1
  • \$\begingroup\$ thank you for the very concise answer! cleared up all of my questions! \$\endgroup\$ Commented Apr 28, 2023 at 20:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.