Event Definitions

Event definitions are used to enable the Dispatcher to consume specific events. Defining an event definition can be done via the following macro:

#pragma once

#include <obscura/core/core.hxx>
#include <obscura/event_system/events/event.hxx>

// NOLINTBEGIN
// This section is raising quite some linting errors.
// Unfortunately there is not much choice than to code it this way as far as I know.
/**
 * A class that can be used to pass string literals as template parameters.
 * @tparam N Number of characters. The template parameter is deduced automatically.
 */
template<size_t N>
struct StringLiteral {
    constexpr StringLiteral(const char (&str)[N]) {
        std::copy_n(str, N, value);
    }

    char value[N] {};
};

// NOLINTEND

template<class Listener, class EventType, void (Listener::*CallbackPointer)(const EventType&)>
class CallbackWrapper {
  public:
    inline static void call(Listener* listener, const EventType& event) {
        (listener->*CallbackPointer)(event);
    }
};

template<StringLiteral Name, class EventType, template<class Listener> class Callback>
    requires std::is_base_of_v<obscura::Event, EventType>
struct EventDefinition {
    using Event = EventType;
    static constexpr obscura::EventCategory Category = Event::Category;
    static constexpr obscura::EventCause Cause = Event::Cause;

    template<class Listener>
    static constexpr auto getCallback(Listener& listener) -> decltype(auto) {
        if constexpr (hasCallback<Listener>) {
            return [&listener](const Event& event) {
                Callback<Listener>::call(&listener, event);
            };
        } else {
            return []([[maybe_unused]] const Event& event) {
            };
        }
    }

    template<class Listener>
    static constexpr bool hasCallback =
        requires(Listener& listener, const EventType& event) { Callback<Listener>::call(&listener, event); };

    inline static constexpr auto getEventCode() -> std::size_t {
        return eventCode(Category, Cause);
    }

    inline static constexpr auto getName() -> std::string_view {
        return std::string_view { Name.value };
    }
};

template<class T>
concept EventDefinitionConcept = requires(T definition) {
    { EventDefinition { definition } } -> std::same_as<T>;
};

Key Event Definition

The event definition for key events are implemented as follows:

#pragma once

#include <obscura/event_system/event_definitions/event_definition.hxx>
#include <obscura/event_system/events/key_events.hxx>

namespace obscura {
template<class Listener>
using KeyPressedCallback = CallbackWrapper<Listener, KeyPressedEvent, &Listener::onKeyPressed>;
using KeyPressedDefinition = EventDefinition<"KeyPressedDefinition", KeyPressedEvent, KeyPressedCallback>;

template<class Listener>
using KeyReleasedCallback = CallbackWrapper<Listener, KeyReleasedEvent, &Listener::onKeyReleased>;
using KeyReleasedDefinition = EventDefinition<"KeyReleasedDefinition", KeyReleasedEvent, KeyReleasedCallback>;

template<class Listener>
using KeyRepeatedCallback = CallbackWrapper<Listener, KeyRepeatedEvent, &Listener::onKeyRepeated>;
using KeyRepeatedDefinition = EventDefinition<"KeyRepeatedDefinition", KeyRepeatedEvent, KeyRepeatedCallback>;
}  // namespace obscura

Example

This example shows how key events can be used. The example implements a listener for key events, registers it with the dispatcher and then sends various events to the dispatcher and dispatches them.

#include <obscura/obscura.hxx>

using namespace obscura;
using namespace std::chrono_literals;

class Listener {
  public:
    static void onKeyPressed(const KeyPressedEvent& event) {
        logInfo("Key {} pressed", event.getKey());
    }

    static void onKeyReleased(const KeyReleasedEvent& event) {
        logInfo("Key {} released", event.getKey());
    }

    static void onKeyRepeated(const KeyRepeatedEvent& event) {
        logInfo("Key {} repeated", event.getKey());
    }

    static void onWindowClosed(const WindowEvent& /*unused*/) {
        Scheduler<Dispatcher>::getInstance().stopMainLoop();
    }

    // Only required to suppress warnings because otherwise there is no listener that
    // processes tick event_system
    void onTick(const TickEvent& event) {
    }
};

auto main() -> int {
    try {
        auto keyPressedEvent =
            KeyPressedEvent { 0 };  // NOLINT(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
        auto keyReleasedEvent =
            KeyReleasedEvent { 1 };  // NOLINT(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
        auto keyRepeatedEvent =
            KeyRepeatedEvent { 42 };  // NOLINT(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)

        auto listener = Listener {};

        auto* scheduler = &Scheduler<Dispatcher>::getInstance();

        scheduler->registerListener(listener);

        scheduler->scheduleOnce(WindowEvent { EventCause::WindowClosed }, 1s);
        scheduler->scheduleOnce(keyPressedEvent);
        scheduler->scheduleOnce(keyReleasedEvent);
        scheduler->scheduleOnce(keyRepeatedEvent);

        scheduler->startMainLoop();
    } catch (const std::exception& e) {
        logError("Exception caught: {}", e.what());
    }

    return EXIT_SUCCESS;
}

Mouse Event Definition

Mouse events are emitted whenever a mouse movement or mouse click is being done. The event definition for mouse events are implemented as follows:

#pragma once

#include <obscura/event_system/event_definitions/event_definition.hxx>
#include <obscura/event_system/events/mouse_events.hxx>

namespace obscura {
template<class Listener>
using MouseMovedCallback = CallbackWrapper<Listener, MouseMovedEvent, &Listener::onMouseMoved>;
using MouseMovedDefintion = EventDefinition<"MouseMovedDefintion", MouseMovedEvent, MouseMovedCallback>;

template<class Listener>
using MouseClickedCallback = CallbackWrapper<Listener, MouseClickedEvent, &Listener::onMouseClicked>;
using MouseClickedDefinition = EventDefinition<"MouseClickedDefinition", MouseClickedEvent, MouseClickedCallback>;
}  // namespace obscura

Example

This example shows how key events can be used. The example implements a listener for mouse events, registers it with the dispatcher and then sends various events to the dispatcher and dispatches them.

#include <obscura/obscura.hxx>

using namespace obscura;
using namespace std::chrono_literals;

class Listener {
  public:
    static void onMouseClicked(const MouseClickedEvent& event) {
        logInfo("Mouse key {} pressed", event.getKey());
        logInfo("Mouse moved to x: {}, y: {}", event.getPosition().x, event.getPosition().y);
    }

    static void onMouseMoved(const MouseMovedEvent& event) {
        logInfo("Mouse moved to x: {}, y: {}", event.getCurrentPosition().x, event.getCurrentPosition().y);
        logInfo("Mouse moved from x: {}, y: {}", event.getPreviousPosition().x, event.getPreviousPosition().y);
        logInfo("Mouse moved by {} horizontal pixels, vertical {} pixels", event.getOffset().x, event.getOffset().y);
    }

    static void onWindowClosed(const WindowEvent& /*unused*/) {
        Scheduler<Dispatcher>::getInstance().stopMainLoop();
    }

    // Only required to suppress warnings because otherwise there is no listener that
    // processes tick event_system
    void onTick(const TickEvent& /*unused*/) {
    }
};

auto main() -> int {
    try {
        auto mouseClickedEvent =
            MouseClickedEvent { 2, 3, 42 };  // NOLINT(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
        auto mouseMovedEvent =
            MouseMovedEvent { 0,
                              5,
                              10,
                              25 };  // NOLINT(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)

        auto listener = Listener {};

        auto* scheduler = &Scheduler<Dispatcher>::getInstance();

        scheduler->registerListener(listener);

        scheduler->scheduleOnce(WindowEvent { EventCause::WindowClosed }, 1s);
        scheduler->scheduleOnce(mouseClickedEvent);
        scheduler->scheduleOnce(mouseMovedEvent);

        scheduler->startMainLoop();
    } catch (const std::exception& e) {
        logError("Exception caught: {}", e.what());
    }

    return EXIT_SUCCESS;
}

Loop Event Definition

Loop events are triggered by the game loop(s). The events that are emitted are ``TickEvent``s that are either emitted for each tick or emitted at the end of the loop. Here is how loop events are implemented

#pragma once

#include <obscura/event_system/event_definitions/event_definition.hxx>
#include <obscura/event_system/events/loop_events.hxx>

namespace obscura {
template<class Listener>
using TickCallback = CallbackWrapper<Listener, TickEvent, &Listener::onTick>;
using TickDefinition = EventDefinition<"TickDefinition", TickEvent, TickCallback>;

template<class Listener>
using LoopEndCallback = CallbackWrapper<Listener, LoopStoppedEvent, &Listener::onLoopStop>;
using LoopEndDefinition = EventDefinition<"LoopEndDefinition", LoopStoppedEvent, LoopEndCallback>;
}  // namespace obscura

Example

#include <obscura/obscura.hxx>

using namespace obscura;
using namespace std::chrono_literals;

class Listener {
  public:
    void onTick(const TickEvent& event) {
        logInfo("The Frame took {} microseconds", event.getDurationSinceLastTick().count());
        ++tickCounter;
        logInfo("This is tick #{}", tickCounter);
    }

    static void onLoopStop(const TickEvent& /*unused*/) {
        logInfo("The loop stopped and I can clean stuff up!");
    }

    static void onWindowClosed(const WindowEvent& /*unused*/) {
        Scheduler<Dispatcher>::getInstance().stopMainLoop();
    }

  private:
    std::size_t tickCounter = 0;
};

auto main() -> int {
    try {
        auto listener = Listener {};

        auto* scheduler = &Scheduler<Dispatcher>::getInstance();

        scheduler->registerListener(listener);

        scheduler->scheduleOnce(WindowEvent { EventCause::WindowClosed }, 1s);

        scheduler->startMainLoop();
    } catch (const std::exception& e) {
        logError("Exception caught: {}", e.what());
    }

    return EXIT_SUCCESS;
}

Window Event Definition

Window events are emitted whenever there is a change to the window. The events are defined as follows:

#pragma once

#include <obscura/event_system/event_definitions/event_definition.hxx>
#include <obscura/event_system/events/window_events.hxx>

namespace obscura {
template<class Listener>
using WindowClosedCallback = CallbackWrapper<Listener, WindowEvent, &Listener::onWindowClosed>;
using WindowClosedDefintion = EventDefinition<"WindowClosedDefintion", WindowEvent, WindowClosedCallback>;
}  // namespace obscura

Example

#include <obscura/obscura.hxx>

using namespace obscura;
using namespace std::chrono_literals;

class Listener {
  public:
    explicit Listener(Window<HeadlessWindow>& window)
      : window(&window) {
    }

    void onWindowClosed(const WindowEvent& /*unused*/) {
        logInfo("Close window!");
        window->close();
        logInfo("Window was closed!");
        Scheduler<Dispatcher>::getInstance().stopMainLoop();
    }

    // Only required to suppress warnings because otherwise there is no listener that
    // processes tick event_system
    void onTick(const TickEvent& /*unused*/) {
    }

  private:
    Window<HeadlessWindow>* window;
};

auto main() -> int {
    try {
        auto& scheduler = Scheduler<Dispatcher>::getInstance();

        auto window = Window<HeadlessWindow> { WindowProperties {} };
        auto listener = Listener { window };
        scheduler.registerListener(listener);

        scheduler.scheduleOnce(WindowEvent { EventCause::WindowClosed }, 1s);
        scheduler.startMainLoop();
    } catch (const std::exception& e) {
        logError("Exception caught: {}", e.what());
    }
}

Custom Event Definition

For creating a custom event that can be used with Obscura you have to implement a respective event definition and pass the definition to the Dispatcher. The following example shows how to define a custom event definition and how to use it in combination with the dispatcher:

#include <obscura/obscura.hxx>
#include <utility>

class CustomEvent: public obscura::Event {
  public:
    explicit CustomEvent(std::string message)
      : obscura::Event(obscura::EventCategory::None, obscura::EventCause::None),
        message(std::move(message)) {
    }

    [[nodiscard]] auto getMessage() const -> const std::string& {
        return message;
    }

  private:
    std::string message;
};

template<class Listener>
using CustomEventCallback = CallbackWrapper<Listener, CustomEvent, &Listener::onCustomEvent>;
using CustomEventDefinition = EventDefinition<"CustomEventDefinition", CustomEvent, CustomEventCallback>;

class CustomListener {
  public:
    void onCustomEvent(const CustomEvent& event) {  // NOLINT(readability-convert-member-functions-to-static)
        // The function cannot be static because it needs to be registered as a callback.
        obscura::logInfo("{}", event.getMessage());
    }
};

auto main() -> int {
    try {
        auto event = CustomEvent("I traveled through the dispatcher!");

        auto listener = CustomListener();

        // All event definitions that are used in a project should be passed to the
        // ConfigurableDispatcher template. Here, we only need one event definition.
        auto dispatcher = obscura::ConfigurableDispatcher<CustomEventDefinition>();
        dispatcher.registerListener(listener);
        dispatcher.enqueue(event);

        obscura::logInfo("Event is in queue but not yet dispatched!");

        dispatcher.dispatch();

        obscura::logInfo("Event was dispatched!");
    } catch (const std::exception& e) {
        obscura::logError("Exception caught: {}", e.what());
    }

    return EXIT_SUCCESS;
}