Message bus

The message bus lets plugins and game code tell each other what happened without knowing each other. A sender broadcasts a message on a channel (a gameplay tag) with a payload (any USTRUCT) and a context (who and what it is about); everybody listening on the channel receives it.

Use it for loose reactions: a notification when a quest finishes, a sound when an item is picked up, a statistic when an enemy dies. A typed integration between two Pulse plugins (one plugin calling the other’s API) still belongs into a bridge plugin.

Channels

  • Pulse plugins send on Pulse.Message.<Plugin>.<Event>, e.g. Pulse.Message.Quest.Finished. Each plugin documents its channels and payload structs; Message channels lists the channels of all Pulse plugins with what their context holds.
  • Games use their own root tag, e.g. Game.Message.ChestOpened.
  • PulseFoundation sends Pulse.Message.Settings.Changed (payload FPulseSettingChangedMessage, on clients; the controller of the local player is the player of the context, the setting its tag).

Channel register

Every plugin registers its channels when its module starts, with payload type, where they are broadcast and one sentence on what a message means. Checks (the value path of a message trigger), channel pickers, docs and sounds read the register; Pulse.Messages.List prints it (Console).

// StartupModule of the module that broadcasts the channel
Pulse::Messages::RegisterChannel<FMyChestMessage>(TAG_Game_Message_ChestOpened, EPulseMessageOrigin::Server,
	NSLOCTEXT("MyGame", "ChestOpened", "A player opened a chest."));

// ShutdownModule
Pulse::Messages::UnregisterChannel(TAG_Game_Message_ChestOpened);
Origin Messages are broadcast
Server On the server and in standalone: game events (a quest finished, an item crafted)
Client On clients: local events (a menu opened, a setting changed)
Both On every machine, each broadcasts its own

Context

Every message can carry an FPulseContext (Conditions and context): the player, who caused it, what it is aimed at, where it comes from, tags and an amount. Listeners and message triggers can filter by it without knowing the payload type (“an enemy killed by this player”, “an item of this kind added”).

Fill what you know. The amount is the number the channel names (items added, damage dealt); a message without an amount of its own carries 1, so a trigger that adds up values counts the messages.

FPulseContext context;
context.Player = PlayerController;
context.Target = Chest;
context.Amount = 1.0;
Messages->Broadcast(TAG_Game_Message_ChestOpened, FMyChestMessage{ Chest }, context);

A message without context gets an empty one. In Blueprint, Broadcast Pulse Message has a Context pin (Make Pulse Context) that can stay empty, and Listen For Pulse Message gives the context next to the payload.

Listening

Match Receives
Exact Messages on exactly this channel
Partial Messages on this channel and all channels below it (Pulse.Message.Quest receives Pulse.Message.Quest.Finished)

Exact listeners are called first, then partial listeners from the most specific channel upwards.

A listener can require a payload type: it then only receives messages whose payload is that type or derives from it. The typed C++ Listen<T> does this for you.

// C++: typed
Handle = Messages->Listen<FPulseSettingChangedMessage>(Pulse::Foundation::Tags::Message_Settings_Changed,
	[this](FGameplayTag Channel, const FPulseSettingChangedMessage& Message, const FPulseContext& Context) { ... });

// C++: removed automatically once Owner is destroyed
Messages->ListenWeak(this, Channel, [this](FGameplayTag Channel, const FInstancedStruct& Payload, const FPulseContext& Context) { ... }, EPulseMessageMatch::Partial);

In Blueprint, Listen For Pulse Message fires On Message Received until you call Cancel or its world context object is destroyed. Read the payload with Get Instanced Struct Value and the context with Break Pulse Context.

Rules while a message is delivered:

  • A listener removed by an earlier listener of the same message is not called anymore.
  • A listener added during a message receives the next message, not the current one.
  • All listeners share one context. With several listeners every listener gets the player id of the context (Ids), so a listener after one that destroyed the pawn still knows the player through GetPlayerId() (the Player pointer is empty then).

Message triggers

A message trigger (FPulseMessageTrigger, Messages/PulseMessageTrigger.h) describes something that happens as data, so designers set it up on an asset: achievements count enemies killed with it, quests the items gathered.

Field What it does
Channel, Match The channel, exact or with its child channels.
Conditions Only messages whose context passes (conditions). While they are checked, the payload of the message is the Extra of the context, so conditions can read it. Unknown does not trigger.
ValuePath The value of a message: empty for the amount of the context, else the path of a number in the payload (Count, Item.StackCount: names with dots through nested structs; numbers, bools as 1 or 0, enums).
bServerOnly Only on the server and in standalone (default): clients do not listen.
bIncludeCatchUp Also messages that only catch up with an earlier state (catching up); off by default, so a counter does not count a loaded or replicated state again.
// Listens until Owner is destroyed or UnbindTrigger; invalid handle on clients for server-only triggers
Handle = Pulse::Messages::BindTrigger(Owner, Trigger,
	[this](FGameplayTag Channel, const FInstancedStruct& Payload, const FPulseContext& Context, double Value) { Count += Value; });

Matches and GetValue check a message by hand, for systems that listen once and check many triggers. In Blueprint, Wait For Pulse Message Trigger fires On Triggered with channel, payload, context and value, until Cancel or once with Only Trigger Once.

Assets that hold a trigger call Trigger.Validate(Context, TEXT("Path.Of.Trigger")) in IsDataValid: it reports a missing channel, a value path that the registered payload of the channel does not have, and a server-only trigger on a channel that is only broadcast on clients.

Scope and networking

Messages stay inside one game instance: in PIE every client and the server have their own bus. To send a message to other machines, put a UPulseMessageRelayComponent on an actor:

Function Where the message is broadcast
RelayToAll (component on the game state or another always relevant actor) Server and every client
RelayToOwner (component on a player controller) Server and the owning client
ForwardToClients (component on the game state) Every client, not the server again: for a message the server already broadcast. Unreliable, for reactions such as sounds (Pulse Sound forwards the server channels its reactions use)

Only the server relays; payload and context are replicated like any RPC parameter: objects arrive only if they are replicated or have stable names. The player id travels with the context (Ids): on a client an empty player (another player’s controller, a pawn that is not relevant) is found again as its player state, and a source path its maker stored as the loaded asset. Instigator, target and payload objects that do not arrive stay empty. Relay To All delivers the same context, with the player id, on the server and the clients, in the server’s order. In Blueprint the Payload and Context pins of Broadcast Pulse Message, Relay To All and Relay To Owner can stay empty: the channel alone is the message.

Client listeners of relayed messages: the player can arrive as its player state, which has no place in the world (its location is the origin). Use Get Context Pawn (null for far players) or the context Location to place effects. Message triggers on clients evaluate their conditions against that player state. A player the client cannot find (its player state has not arrived yet, or it left) stays empty, with its id in the context.

Example: a switch that sends its color to every machine, and a lamp that listens for it (whole scene on the Examples page).

Debugging

  • Every message is written into the debug timeline (Messages source) while the timeline records (bRecordMessagesInTimeline).
  • bLogMessages logs every message to LogPulseFoundation (Verbose).
  • Console: Pulse.Messages.List prints the channel register with the listeners of the game, PulseMessageListeners prints the listeners per channel, PulseMessageSend <Channel> sends a message without payload.
  • The timeline and the log name the player of the context and its player id.

PulseFoundation plugin documentation.

This site uses Just the Docs, a documentation theme for Jekyll.