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.
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
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.
#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);
}
}
Events::towerFire(i) guarantees both the tower and the button generate identical strings. No risk of "tower_fire_2" vs "tower.fire.2" mismatches.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.
#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;
};
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).
// 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>.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.
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;
};
Step 5 — The Application
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); });
}