2
\$\begingroup\$

I have written a simple event handling system, which is intended for a game but right now I am just trying to get a general framework created. This is simply for practice for myself, but I am trying to design it as if someone else possibly had to use it and maintain it.

I would appreciate any comments on what I can do to improve in the sense of good practice, readability, usability, and generally if things make sense. I have been reading C++ Core Guidelines and I have been trying to (mostly) follow them.

It starts out with an interface for events:

#pragma once

struct IEvent
{
    virtual ~IEvent() = default;
};

Events are only intended to be bags of unalterable data, nothing more. Here are two events:

struct CloseWindowEvent : public IEvent
{
    virtual ~CloseWindowEvent() = default;
};

#include "../../Input Handling/Keyboard.h"

struct KeyInputEvent : public IEvent
{
    const Key m_key;
    const KeyAction m_keyAction;

    explicit KeyInputEvent(const Key& key, const KeyAction& action)
        : m_key(key), m_keyAction(action)
    { }

    virtual ~KeyInputEvent() = default;
};

An "entity" that wishes to handle events needs to use EventHandler. Right now it has to inherit this class but I intend to have it as a component instead in the future.

#include "Interfaces\IEvent.h"
#include "Interfaces\IHandlerFunction.h"

#include "Implementations\MemberFunctionHandler.h"

#include "../../../Logger/LoggerLocator.h"

#include <memory>
#include <type_traits>
#include <typeindex>
#include <unordered_map>

class EventHandler
{
public:
    virtual ~EventHandler() = default;

    void handleEvent(IEvent& event) const
    {
        std::type_index eventID(typeid(event));

        if (m_functionHandlerMap.count(eventID))
        {
            m_functionHandlerMap.at(eventID)->executeFunction(event);
        }
        else
        {
            // TODO: Get event name.
            LoggerLocator::getLogger().warn("The event {event name} was not handled as there is no function associated with it.", LineFileInfo());
        }
    }

protected:
    EventHandler() = default;

    /*
        Note: Any function that has already been binded to a particular event will be overriden
        if another function tries to bind to the same event.
    */
    template<typename EventT, typename FunctionOwnerT, typename FunctionT>
    void bindToEvent(const FunctionOwnerT& functionOwner, const FunctionT& function)
    {
        static_assert(std::is_base_of<IEvent, EventT>::value, "EventT must inherit from IEvent.");

        m_functionHandlerMap.insert
        (
            std::make_pair
            (
                std::type_index(typeid(EventT)),
                std::make_unique<MemberFunctionHandler<EventT&>>(functionOwner, function)
            )
        );

    }

private:
    std::unordered_map<std::type_index, std::unique_ptr<IHandlerFunction>> m_functionHandlerMap;

};

This stores function handlers to events and calls them when an event is sent. Right now I only intend for member functions to be used as handlers. The interface for the handlers:

#include "IEvent.h"

class IHandlerFunction
{
public:
    virtual ~IHandlerFunction() = default;

    virtual void executeFunction(IEvent& event) const = 0;
};

And an implementation:

#include "../Interfaces/IHandlerFunction.h"

#include "../../../../Logger/LoggerLocator.h"

#include <functional>

template<typename EventT>
class MemberFunctionHandler : public IHandlerFunction
{
public:
    template<typename FunctionOwnerT, typename FunctionT>
    explicit MemberFunctionHandler(const FunctionOwnerT& functionOwner, const FunctionT& function)
    {
        m_function = std::bind(function, functionOwner, std::placeholders::_1);
    }

    virtual ~MemberFunctionHandler() = default;

    virtual void executeFunction(IEvent& event) const
    {
        try
        {
            m_function(dynamic_cast<EventT&>(event));
        }
        catch (std::bad_cast exception)
        {
            LoggerLocator::getLogger().error(
                "Executed event is not the same type as expected for this handler. Nothing was done",
                LineFileInfo());
        }
    }

private:
    using EventCallback = std::function<void(EventT&)>;

    EventCallback m_function;
};

I tested it by having another part of my code fire events when keys were pressed, which would make the game/application close - they were successful in this. For lack of a better name, a "MenuController" registers its functions for the two example events. MenuController inherits EventHandler.

#include "../../../Headers/Application/Systems/MenuController.h"
#include "../../../Headers/Application/Systems/Event Handling/EventHandler.h"
#include "../../../Headers/Application/Systems/Input Handling/Keyboard.h"

#include "../../../Headers/Logger/LoggerLocator.h"

MenuController::MenuController()
    : m_gameShouldClose(false)
{   
    EventHandler::bindToEvent<KeyInputEvent>(this, &MenuController::onKeyPressed);
    EventHandler::bindToEvent<CloseWindowEvent>(this, &MenuController::onCloseWindowRequest);
}

void MenuController::onKeyPressed(const KeyInputEvent& keyEvent)
{
    switch (keyEvent.m_key)
    {
    case Key::Escape:
        m_gameShouldClose = true;
        break;
    default:
        // TODO: Add compatibility in Key to say what key was triggered.
        LoggerLocator::getLogger().warn("Key that event handler was not interested in was incorrectly sent.", LineFileInfo());
        break;
    }
}

void MenuController::onCloseWindowRequest(const CloseWindowEvent& keyEvent)
{
    m_gameShouldClose = true; // for now, for simplicity
}

A key input handler fires off some events like so (just for example sake):

void KeyInputHandler::handleInput(const Key& key, const KeyAction& action)
{
    switch (key)
    {
    case Key::A:
        for (auto eventHandler : m_keyEventHandlers.at(key))
        {
            eventHandler->handleEvent(CloseWindowEvent());
        }

        break;
    case Key::Escape:
        for (auto eventHandler : m_keyEventHandlers.at(key))
        {
            eventHandler->handleEvent(KeyInputEvent(key, action));
        }
        break;
    default:
        // TODO: Add compatibility in Key to say what key was triggered.
        LoggerLocator::getLogger().warn("An unknown key was triggered. Key code: {key code}.", LineFileInfo());
        break;
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.