ml::docs | Your First App Manifests Events Messaging Plugins Scenes Resources
See all tutorials →
Advanced 5 sections ~25 min C++17/20

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.

Introduction

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

1

Manifest

Declares the event enum. This is the Malena way — all identity lives in the manifest.

2

Receiver Trait

The class components inherit. Provides friendly callback methods like onMyEvent(f). Stores callbacks via EventReceiver.

3

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() and fire()
  • Use for: keyboard, mouse, joystick, window events

FrameDispatcher — Every-frame

  • Fires unconditionally every frame
  • occurred() is sealed — only implement fire()
  • Use for: animation ticks, polling, physics, AI

1Core Concepts

Step 1 — The Manifest

MyEventManifest.h cpp
#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,
    };
};
You don't need to call set() for event enums — the framework generates unique keys automatically via EnumKey::get(). The manifest just needs the enum declaration.

2Core Concepts

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().

Gamepad.h — receiver trait cpp
#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));
    }
};

3Core Concepts

Step 3 — The Dispatcher

GamepadDispatcher.h — EventDispatcher cpp
#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.

4Core Concepts

Using Your New Trait

PlayerController.h — using the Gamepad trait cpp
// 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);
        });
    }
};
No changes to 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.