Creating Event Traits
Extend the Malena event system with your own event types. Write a manifest, receiver, dispatcher, and ML_EXPORT — the framework handles everything else.
Before you start
Read Built-in Events first.
Three Parts That Work Together
A custom event trait has three pieces: a manifest (the enum), a receiver trait (the friendly callback API), and a dispatcher (the singleton that detects and delivers the event).
Manifest
Declares the event enum. This is the Malena way — all identity lives in the manifest.
Receiver Trait
The class components inherit. Provides friendly callback methods like onMyEvent(f). Stores callbacks via EventReceiver.
Dispatcher
A singleton that detects when the event fires and delivers it via EventManager. Registered with ML_EXPORT so AppManager picks it up automatically.
EventDispatcher — SFML-triggered
- Fires only when a specific SFML input event occurs
- Implement
occurred()andfire() - Use for: keyboard, mouse, joystick, window events
FrameDispatcher — Every-frame
- Fires unconditionally every frame
occurred()is sealed — only implementfire()- Use for: animation ticks, polling, physics, AI
Step 1 — The Manifest
#pragma once
#include
class GamepadManifest : public ml::Manifest
{
public:
// The event enum — components subscribe using these values
enum class Event
{
BUTTON_PRESSED,
BUTTON_RELEASED,
AXIS_MOVED,
};
};
set() for event enums — the framework generates unique keys automatically via EnumKey::get(). The manifest just needs the enum declaration.Step 2 — The Receiver Trait
The receiver trait is what components inherit to get your event API. It inherits ml::EventReceiver and stores callbacks via Fireable::addCallback().
#pragma once
#include "GamepadManifest.h"
#include
#include
class Gamepad : public ml::EventReceiver
{
public:
// Simple overload — no SFML event data
void onButtonPressed(std::function f)
{
ml::Callback cb = [f = std::move(f)](const std::optional&){ f(); };
ml::Fireable::addCallback(GamepadManifest::Event::BUTTON_PRESSED, this, std::move(cb));
}
// Full overload — passes SFML event data
void onButtonPressed(std::function&)> f)
{
ml::Fireable::addCallback(GamepadManifest::Event::BUTTON_PRESSED, this, std::move(f));
}
void onButtonReleased(std::function f)
{
ml::Callback cb = [f = std::move(f)](const std::optional&){ f(); };
ml::Fireable::addCallback(GamepadManifest::Event::BUTTON_RELEASED, this, std::move(cb));
}
void onAxisMoved(std::function&)> f)
{
ml::Fireable::addCallback(GamepadManifest::Event::AXIS_MOVED, this, std::move(f));
}
};
Step 3 — The Dispatcher
#pragma once
#include "GamepadManifest.h"
#include
#include
#include
class GamepadDispatcher : public ml::EventDispatcher
{
public:
// Return true when the SFML event matches your condition
bool occurred(const std::optional& event) override
{
return event.has_value() && (
event->is() ||
event->is() ||
event->is());
}
// Return true to deliver to this component (false to skip it)
bool filter(const std::optional& event, ml::Core* component) override
{
return component != nullptr; // deliver to all components
}
// Deliver the event to all matching subscribers
void fire(const std::optional& event) override
{
if (!event.has_value()) return;
if (event->is())
ml::EventManager::fire(GamepadManifest::Event::BUTTON_PRESSED, this, event);
if (event->is())
ml::EventManager::fire(GamepadManifest::Event::BUTTON_RELEASED, this, event);
if (event->is())
ml::EventManager::fire(GamepadManifest::Event::AXIS_MOVED, this, event);
}
};
// Register with AppManager's event loop — outside any namespace
ML_EXPORT(GamepadDispatcher)
ML_EXPORT must be placed outside any namespace, after the class is fully defined. The dispatcher singleton is constructed once at program startup before onInit() is called.Using Your New Trait
// Your component inherits Gamepad alongside Component
class PlayerController : public ml::Component<>, public Gamepad
{
void onReady() override
{
onButtonPressed([this](const std::optional& e) {
if (!e) return;
if (auto* btn = e->getIf()) {
if (btn->button == 0) jump();
if (btn->button == 1) attack();
}
});
onAxisMoved([this](const std::optional& e) {
if (!e) return;
if (auto* axis = e->getIf())
move(axis->position / 100.f);
});
}
};
AppManager, Core.h, or any framework file needed. ML_EXPORT registers the dispatcher automatically. The framework is designed so new event types require zero changes to existing code.