ml::docs | Your First App Manifests Events Plugins Scenes Graphics Resources
← All Tutorials
Intermediate 6 sections ~30 min C++17/20

Events & Messaging

A complete walkthrough of Malena's two communication channels — string events and typed messages — through a working visual demo application called SignalBoard.

Before you start

Read Built-in Events first.

Introduction

Two Communication Systems

Malena provides two complementary ways for objects to talk to each other. Understanding the difference is the core skill this tutorial teaches, through a complete working application called SignalBoard.

String Events — subscribe / publish

  • Named with a string constant or manifest Event enum
  • Fire-and-forget — no payload
  • Any Subscribable can listen
  • Best for: button clicks, broadcasts, UI signals
  • Example: Events::FIRE_ALL

Typed Messages — onMessage / sendMessage

  • Keyed by a scoped enum value
  • Carries a strongly-typed payload struct
  • Requires the Messenger trait (opt-in)
  • Best for: structured data between components
  • Example: GameMsg::TowerFired + TowerFiredData

1Core Concepts

Step 1 — Define Event Names

Before writing any component, define string constants for every event in your app. One file is the entire event vocabulary.

src/Events.h cpp
#pragma once
#include 

namespace Events
{
    // Broadcast — one sender reaches all towers
    inline constexpr const char* FIRE_ALL  = "tower.fire_all";
    inline constexpr const char* RESET_ALL = "tower.reset_all";

    // Per-tower — each tower subscribes to its own ID string
    inline std::string towerFire(int id)
    {
        return "tower.fire." + std::to_string(id);
    }
}
Using a helper function like Events::towerFire(i) guarantees both the tower and the button generate identical strings. No risk of "tower_fire_2" vs "tower.fire.2" mismatches.

2Core Concepts

Step 2 — Define Typed Messages

When a component needs to send data alongside a signal, use typed messages. The payload is a plain struct. The enum key ties sender and receiver without direct coupling.

src/Messages.h cpp
#pragma once
#include 
#include 

// One enum for all typed messages in the application
enum class GameMsg
{
    TowerFired,  // payload: TowerFiredData
    ScoreUpdate, // payload: int
};

// Pure data — no methods, no logic
struct TowerFiredData
{
    int         id;
    sf::Color   color;
    std::string name;
};

3Core Concepts

Step 3 — The Hybrid Component

SignalTower uses both systems. It subscribes to string events (via Subscribable) and sends typed messages when it fires (via Messenger).

src/SignalTower.h — key methods cpp
// Opt in to Messenger explicitly as a template argument
class SignalTower : public ml::Component
{
public:
    SignalTower(int id, const std::string& name, sf::Color color)
        : _id(id), _name(name), _color(color)
    {
        // Subscribe to string events in the constructor
        subscribe(Events::FIRE_ALL,       [this]() { activate(); });
        subscribe(Events::towerFire(_id), [this]() { activate(); });
        subscribe(Events::RESET_ALL,      [this]() { deactivate(); });
    }

private:
    void activate()
    {
        _active = true;

        // Send a typed message — no knowledge of who receives it
        sendMessage(GameMsg::TowerFired,
            TowerFiredData{ _id, _color, _name });
    }
};
ml::Component<> gives you Subscribable but NOT Messenger. To get sendMessage/onMessage you must write ml::Component<ml::Messenger>.

4Core Concepts

Step 4 — Receiver and Chain Sender

EventLog receives a typed message and chains a second message downstream. A component can be both receiver and sender.

src/EventLog.h — constructor excerpt cpp
class EventLog : public ml::Component
{
public:
    EventLog()
    {
        // Register a typed message listener
        onMessage(GameMsg::TowerFired,
            [this](const TowerFiredData& data)
            {
                ++_total;

                // data fields are directly accessible — no cast, no void*
                addEntry("[" + data.name + "] fired  (total: " +
                         std::to_string(_total) + ")", data.color);

                // Chain: EventLog is also a sender
                sendMessage(GameMsg::ScoreUpdate, _total);
            });
    }
private:
    int _total = 0;
};

5Core Concepts

Step 5 — The Application

src/SignalBoard.cpp — registerEvents() cpp
void SignalBoard::registerEvents()
{
    // Button callbacks publish string events — static call, no instance needed
    _btnA.onClick([]() { ml::Subscribable::publish(Events::towerFire(0)); });
    _btnB.onClick([]() { ml::Subscribable::publish(Events::towerFire(1)); });
    _btnC.onClick([]() { ml::Subscribable::publish(Events::towerFire(2)); });
    _btnD.onClick([]() { ml::Subscribable::publish(Events::towerFire(3)); });

    // Broadcast — one click reaches every subscribed tower
    _btnFireAll.onClick([]() { ml::Subscribable::publish(Events::FIRE_ALL);  });
    _btnReset.onClick  ([]() { ml::Subscribable::publish(Events::RESET_ALL); });
}
ml::Subscribable::publish() is static. Any code anywhere — a button callback, a timer, a network handler — can publish a named event. The framework finds every registered subscriber and notifies them.