-
Notifications
You must be signed in to change notification settings - Fork 0
Message System
The only legal way for plugins to talk to each other. By design.
PGX enforces a strict architectural rule: no plugin may depend on any other plugin at runtime. All plugins depend on the core, and nothing else. This is the star topology -- one hub, many spokes, no spoke-to-spoke connections.
+-------+
+------>| Core |<------+
| +-------+ |
| ^ |
| | |
+------+ +------+ +------+
| Save | | Audio| | Load | ... (21 plugins)
+------+ +------+ +------+
^ ^ ^
| | |
X-----------X-----------X
No direct connections
This topology guarantees that plugins can be added, removed, or replaced independently. A project that does not need the Audio system can exclude it without breaking anything else. A project that replaces the Save system with a custom implementation does not invalidate any other plugin's code.
But systems still need to communicate. Real examples from a shipping game framework:
- The Save system needs to know when game state changes so it can mark the save as dirty.
- The Audio system needs to know when a loading screen appears so it can mute gameplay audio.
- The Event Handler needs to trigger auto-save, which lives in the Save system.
- Game code needs to broadcast custom events that multiple systems react to.
Without a communication mechanism, the star topology becomes an isolation ward. Every system operates in its own bubble, and cross-system behavior requires the game code to manually orchestrate everything.
The naive solution -- a global event bus with string-based channels and untyped payloads -- works until the first production debugging session. When every message is void* and every channel is a string that nobody validates, the debugging experience is "something fired an event with data that might be what I expected, or might be garbage from a typo in the channel name."
The Message System provides a typed pub/sub bus that is the sole permitted mechanism for cross-plugin runtime communication. It lives in the core module, available to all plugins.
The design is based on Epic's Lyra messaging pattern -- the same architecture that ships in Lyra for cross-system communication. PGX extends it with message history, channel statistics, partial tag matching, and configuration via Data Asset.
Publisher Message Bus Listeners
(any code) (GameInstance scope) (any code)
+-----------+ +-------------------+ +-----------+
| System A |---broadcast--->| Channel: X |---notify--->| System B |
| | payload T | Listener 1 (T) | | |
+-----------+ | Listener 2 (T) | +-----------+
| Listener 3 (any)|
+-----------+ +-------------------+ +-----------+
| System C |---broadcast--->| Channel: Y |---notify--->| System D |
| | payload U | Listener 1 (U) | | |
+-----------+ +-------------------+ +-----------+
| |
| History Buffer |
| Channel Stats |
+-------------------+
The flow:
- A publisher broadcasts a typed payload on a GameplayTag channel.
- The bus iterates all listeners registered on that channel.
- Each listener receives the channel tag and the typed payload.
- The broadcast is recorded in the history buffer.
- Channel statistics are updated.
Type safety:
In C++, broadcasts and listeners are template-typed. The compiler ensures that a listener registered for payload type T only compiles if T matches the broadcast. A listener for type "WeaponFiredPayload" will not accidentally receive a "DoorOpenedPayload."
In Blueprint, the system provides two broadcast mechanisms:
- A simple broadcast that sends a basic message with a sender reference -- covers 90% of use cases where the listener only needs to know "who sent this."
- A struct broadcast with wildcard pins that adapts to any payload struct type -- for the 10% of cases that need typed data.
An async Blueprint node provides the listener side: connect a payload type, and the output pins morph to match. When a matching message arrives, the node fires.
Channels are GameplayTags, not strings. This means:
- Auto-complete in the editor. No typos.
- Hierarchical organization. A listener on "Audio" receives messages from "Audio.Music" and "Audio.SFX" (when partial matching is enabled).
- Validation at edit time. A tag that does not exist in the tag table is flagged immediately.
When enabled (default), a listener registered on a parent tag receives broadcasts on any child tag.
Listener registered on: PGX.Message.Audio
|
+-- Receives: PGX.Message.Audio.MusicChanged (child match)
+-- Receives: PGX.Message.Audio.VolumeChanged (child match)
+-- Receives: PGX.Message.Audio (exact match)
+-- Does NOT receive: PGX.Message.Save.Completed (different branch)
This enables broad listeners ("tell me about any audio event") and specific listeners ("tell me only about music changes") on the same bus with no additional infrastructure.
Partial matching can be disabled per-project via the config Data Asset for projects that want strict exact-match semantics.
Every broadcast is recorded in a circular buffer with configurable capacity (default: 100 messages per channel). History records include:
- Channel tag
- Payload type name
- Timestamp
- Number of listeners notified
This history is queryable at runtime and visible in the inspector. When debugging "did that message actually fire?", the answer is in the history buffer, not in a print statement.
The system tracks aggregate statistics:
- Total broadcasts across all channels
- Total listeners notified
- Number of active channels
- Number of active listeners
- History buffer utilization
These statistics feed the inspector and are available via console commands.
A single config Data Asset controls system behavior:
| Setting | Default | Purpose |
|---|---|---|
| Max Message History | 100 | Messages retained per channel in the history buffer |
| Log Broadcasts | false | Log every broadcast at verbose level (useful for debugging, noisy in production) |
| Log Registrations | false | Log every listener register/unregister |
| Enable Partial Matching | true | Listeners on parent tags receive child tag broadcasts |
Zero-config default: if no config DA exists, the system initializes with the defaults above. Partial matching on, 100-message history, no verbose logging.
+---------------------------------------------------------------+
| PGX Message Inspector |
+---------------------------------------------------------------+
| |
| Active Channels: 7 Active Listeners: 23 |
| Total Broadcasts: 1,847 History Buffer: 42% |
| |
| Channel | Listeners | Last Broadcast |
| PGX.Message.Save.Dirty | 3 | 0.4s ago |
| PGX.Message.Audio.Music | 2 | 1.2s ago |
| PGX.Message.GameFlow.State | 5 | 3.7s ago |
| PGX.Message.Loading.Start | 4 | 12.1s ago |
| ... | | |
| |
| --- Message History (last 20) --- |
| [14:32:01.847] Audio.Music MusicStatePayload (2 recv) |
| [14:32:01.203] Save.Dirty BasicMessage (3 recv) |
| [14:31:59.991] GameFlow.State FlowStatePayload (5 recv) |
| |
| [Broadcast Test] Channel: [________] [Send] |
+---------------------------------------------------------------+
The inspector shows:
- All active channels with listener counts
- Recent message history with timestamps, types, and receiver counts
- Aggregate statistics
- A test broadcast button for debugging
pgx.message.status Total channels, listeners, history size
pgx.message.channels All active channels with listener counts
pgx.message.history Recent history (optional channel filter)
pgx.message.broadcast Test-broadcast on a given channel
This is not optional. The Message System is the ONLY way for L2 plugins to communicate with each other at runtime. This is enforced by architecture (no L2 module can have a build dependency on another L2 module) and by convention (code review rejects direct cross-plugin calls).
The practical pattern:
Save System Audio System
(L2 Plugin) (L2 Plugin)
| ^
| |
v |
[Broadcast on [Listens on
PGX.Message.Save.Completed] PGX.Message.Save.Completed]
| ^
| |
+---------> Message Bus --------+
(L1 Core)
Neither system knows the other exists. Neither system has a build dependency on the other. If the Audio system is removed from the project, the Save system still broadcasts, and the messages go to zero listeners. No errors, no crashes, no dangling references.
The Event Handler's built-in handlers use the Message System to communicate with L2 systems. The AutoSave handler does not call the Save subsystem directly -- it broadcasts a message that the Save system listens to. This preserves the star topology even for framework-internal behaviors.
The Message System is the primary mechanism for game-level pub/sub events. Custom game channels work identically to framework channels. A "Player.Respawned" channel, a "Quest.Completed" channel, a "Shop.Purchase" channel -- all use the same infrastructure.
| Event | When It Fires |
|---|---|
| Message Broadcast | Every broadcast (includes channel tag and payload type name) |
| Listener Registered | A new listener subscribes to a channel |
| Listener Unregistered | A listener unsubscribes from a channel |
| Message Broadcast (Native) | Every broadcast (includes struct type and timestamp, for C++ monitoring) |
The Message System is the architectural keystone that makes star topology viable. Without it, decoupled plugins cannot communicate, and the architecture devolves into either monolithic coupling or manual orchestration in game code.
The Lyra-based design is not an accident. Epic faced the same problem in Lyra -- modular gameplay features that need to communicate without direct dependencies -- and solved it with a typed message bus on GameplayTag channels. PGX takes that proven pattern, extends it with history and statistics for production debugging, and makes it configurable via Data Asset.
For framework users, the Message System is invisible infrastructure. They broadcast when something happens, listen when they care about something, and never think about how the message gets from A to B. That transparency is the goal.
For framework developers, the Message System is the enforcement mechanism for architectural integrity. Every time two plugins need to talk, the answer is always the same: message bus. No exceptions. No special cases. One pattern, universally applied.
- Development Preview
- Getting Started
- Release branch catalog
- Public Plugin Matrix
- Early Preview Plugins
- Known Issues
- Architecture Overview
- Plugin Topology
- Module Reference
- Configuration and Registry
- Data-Driven Design
- Profiles and Budgets
- Gameplay Tag Architecture
- Initialization Pipeline
- Cross-Plugin Communication
- Message System
- Event Handlers
- Logging and Trace
- Runtime Flows
- Blueprint API Design
- Editor Integration
- Editor Visual System