User Documentation · Version 1.0.0 · Unreal Engine 5.8
A high-performance, fully decoupled signal & message broker built on hierarchical
Gameplay Tags. Systems, actors and UI communicate without ever holding a hard
reference to each other — broadcast a signal on a tag channel, subscribe wherever you
like, in C++ or Blueprints.
| Item | Detail |
|---|---|
| Engine | Unreal Engine 5.8 (EngineVersion 5.8.0) |
| Module type | Runtime, LoadingPhase: PreDefault |
| Language | C++ (source included) and Blueprints |
| Engine dependencies | Core, CoreUObject, Engine, GameplayTags only |
| Third-party plugin deps | None — fully self-contained |
| Content | Optional (CanContainContent: true); the module ships no required content |
Supported platforms. SignalNexus is pure gameplay-framework C++ with no
platform-specific code, no native/third-party libraries and no editor-only
dependencies. It therefore builds and runs on all UE-supported platforms,
including macOS, Linux, Android, iOS and the major consoles. The .uplugin
nevertheless lists Win64 only in its PlatformAllowList, because Win64 is
the one platform this release was actually built and tested on. Add a platform to
that list and rebuild if you target it yourself — the source ships with the plugin.
SignalNexus folder into your project's Plugins/ directory:YourProject/Plugins/SignalNexus/..uproject → Generate Visual Studio project files.YourProjectEditor from your IDE).Copy SignalNexus into Engine/Plugins/Marketplace/ (or the Fab install location)
so it is available to every project on that engine install.
Open the Output Log and confirm you see the module load line under the
LogSignalNexus category on startup. If the category never appears, the module did
not load — see Troubleshooting.
FGameplayTag that names the topic of a signal, e.g.Event.Player.Died. Channels are hierarchical: a subscription on Event.PlayerEvent.Player.Died, Event.Player.Spawned, and any deeper child.FSignalNexusPayload. It canFSignalNexusHandle you keep in order to unsubscribe.Immediate,DeferredNextFrame, or Throttled.Everything is hosted by USignalNexusSubsystem, a GameInstance subsystem — one bus
per game instance, alive for the whole session and available from any world context.
No C++ required. A designer can send and receive any struct — including
Blueprint-only structs — with zero registration.
Channel tag, connect any struct to theValue pin, and (optionally, under Advanced) pick a Priority andThrottleRate.Channel tag and bind an event to OnSignal.Channel and a Payload. Keep the returned handle.Payload in and read your struct outOutValue pin. The Return Value is false if the stored typeHelper pure nodes: Is Payload Valid, Get Payload Type Name,
Is Valid Signal Handle.
#include "SignalNexusSubsystem.h"
#include "SignalNexusTypes.h"
void AMyActor::BeginPlay()
{
Super::BeginPlay();
USignalNexusSubsystem* Bus = USignalNexusSubsystem::Get(this);
if (!Bus) { return; }
// Subscribe — the callback only fires for signals carrying FSignalDamagePayload.
DamageHandle = Bus->Subscribe<FSignalDamagePayload>(
FGameplayTag::RequestGameplayTag(TEXT("Event.Combat.Damage")),
[this](const FSignalDamagePayload& Dmg)
{
UE_LOG(LogTemp, Log, TEXT("Took %.1f damage"), Dmg.Amount);
});
}
void AMyActor::DealDamage()
{
if (USignalNexusSubsystem* Bus = USignalNexusSubsystem::Get(this))
{
FSignalDamagePayload Dmg;
Dmg.Amount = 25.f;
Dmg.Instigator = this;
Bus->BroadcastSignal(
FGameplayTag::RequestGameplayTag(TEXT("Event.Combat.Damage")),
Dmg, ESignalNexusPriority::Immediate);
}
}
void AMyActor::EndPlay(const EEndPlayReason::Type Reason)
{
if (USignalNexusSubsystem* Bus = USignalNexusSubsystem::Get(this))
{
Bus->Unsubscribe(DamageHandle);
}
Super::EndPlay(Reason);
}
Build.cs — add
"GameplayTags"and this module:PublicDependencyModuleNames.AddRange(new[]{ "SignalNexus", "GameplayTags" });
All public types live in the SignalNexus runtime module and are exported with the
SIGNALNEXUS_API macro.
USignalNexusSubsystem (SignalNexusSubsystem.h)UGameInstanceSubsystem + FTickableGameObject. The global hub.
| Member | Description |
|---|---|
static USignalNexusSubsystem* Get(const UObject* WorldContextObject) |
Convenience accessor from any world-context object. |
template<T> FSignalNexusHandle Subscribe(FGameplayTag Channel, TFunction<void(const T&)> Callback) |
Type-safe subscribe; callback fires only for payloads of type T. |
template<T> void BroadcastSignal(FGameplayTag Channel, const T& Payload, ESignalNexusPriority = Immediate, float ThrottleRate = 30.f) |
Pack a typed struct and broadcast it. |
void Unsubscribe(FSignalNexusHandle Handle) |
Remove a subscription. |
void RegisterInterceptor(FGameplayTag, TScriptInterface<ISignalNexusInterceptor>) |
(BlueprintCallable) Add middleware on a channel subtree. |
void UnregisterInterceptor(FGameplayTag, TScriptInterface<ISignalNexusInterceptor>) |
(BlueprintCallable) Remove middleware. |
FSignalNexusRouter& GetRouter() |
Direct access to the routing core for advanced callers. |
USignalNexusBlueprintLibrary (SignalNexusBlueprintLibrary.h)The Blueprint face. Broadcast Signal and Unpack Signal Payload are custom-thunk
wildcard nodes.
| Node | Description |
|---|---|
| Broadcast Signal | Broadcast any struct (wildcard Value pin) on a channel. |
| Subscribe To Signal | Bind FSignalNexusReceivedDelegate; returns a handle. Auto-removed when the bound object dies. |
| Unpack Signal Payload | Extract a payload into a wildcard OutValue struct; returns false on type mismatch. |
| Unsubscribe From Signal | Remove a subscription by handle. |
| Is Payload Valid / Get Payload Type Name / Is Valid Signal Handle | Pure helpers. |
FSignalNexusPayload (SignalNexusPayload.h)Generic, self-describing, value-semantic container for one reflected struct.
| Member | Description |
|---|---|
template<T> void Pack(const T&) |
Store a typed struct. |
template<T> bool Unpack(T& Out) const |
Extract into T; false (+ warning) on type mismatch. |
void PackRaw(const UScriptStruct*, const void*) / bool UnpackRaw(const UScriptStruct*, void*) const |
Reflection-based pack/unpack (used by thunks). |
bool IsValid() const · const UScriptStruct* GetStructType() const · FString GetStructName() const · template<T> bool IsA() const · void Reset() |
Introspection & lifecycle. |
Correct value semantics (deep copy, proper destruction, GC of contained UObject
references) — safe to copy, move, queue and pass across Blueprint pins.
FSignalNexusRouter (SignalNexusRouter.h)The pure, UObject-free routing core (unit-testable in isolation). Holds subscriptions,
interceptors and scheduling queues; performs hierarchical delivery up the tag parent
chain. The subsystem forwards to it and drives its Tick.
ISignalNexusInterceptor / USignalNexusLambdaInterceptor (SignalNexusInterceptor.h)Middleware interface (BlueprintNativeEvent Intercept) plus a ready-made
lambda-driven implementation for C++/tests.
SignalNexusTypes.h)enum ESignalNexusPriority { Immediate, DeferredNextFrame, Throttled }enum EInterceptorResult { Pass, Block }struct FSignalNexusHandle — opaque subscription id.FSignalIntPayload, FSignalNamePayload,FSignalVectorPayload, FSignalDamagePayload.All plugin logging uses the LogSignalNexus category (SignalNexusLog.h) with proper
Warning/Error levels — no UE_LOG(LogTemp, …) in shipping code paths.
USignalNexusSubsystem* Bus = USignalNexusSubsystem::Get(this);
// Subscribe to the parent channel...
Bus->Subscribe<FSignalNamePayload>(
FGameplayTag::RequestGameplayTag(TEXT("Event.Player")),
[](const FSignalNamePayload& P){ /* fires for ALL Event.Player.* */ });
// ...a broadcast on a child still reaches it:
Bus->BroadcastSignal(
FGameplayTag::RequestGameplayTag(TEXT("Event.Player.Died")),
FSignalNamePayload(TEXT("Hero")));
// Queued now, delivered on the next world tick — avoids re-entrancy during iteration.
Bus->BroadcastSignal(MyTag, FSignalIntPayload(42),
ESignalNexusPriority::DeferredNextFrame);
// At most 10 dispatches/second on this channel; extras coalesce to the latest value.
Bus->BroadcastSignal(HealthBarTag, FSignalIntPayload(CurrentHealth),
ESignalNexusPriority::Throttled, /*ThrottleRate=*/10.f);
#include "SignalNexusInterceptor.h"
USignalNexusLambdaInterceptor* Clamp = NewObject<USignalNexusLambdaInterceptor>(this);
Clamp->InterceptFn = [](FGameplayTag /*Channel*/, FSignalNexusPayload& Payload)
{
FSignalDamagePayload Dmg;
if (Payload.Unpack(Dmg))
{
Dmg.Amount *= 0.5f; // modify in place
Payload.Pack(Dmg); // write it back
}
return EInterceptorResult::Pass; // or EInterceptorResult::Block to drop it
};
USignalNexusSubsystem::Get(this)->RegisterInterceptor(
FGameplayTag::RequestGameplayTag(TEXT("Event.Combat.Damage")), Clamp);
Define S_QuestUpdate in a Blueprint, then:
Channel = Event.Quest.Updated, wire an S_QuestUpdate into Value.Channel = Event.Quest → Unpack Signal Payload with anS_QuestUpdate on the OutValue pin. No C++ touched.| Mode | Behaviour | Use for |
|---|---|---|
Immediate |
Synchronous, on the calling thread, right now. | Most gameplay events. |
DeferredNextFrame |
Queued, flushed on the next world tick by the subsystem's tickable. | Avoiding re-entrancy while iterating; batching. |
Throttled |
Rate-limited per channel; coalesces to at most one dispatch per interval (ThrottleRate = max dispatches/second, default 30). |
High-frequency signals (UI/HUD refresh). |
The subsystem is tickable even when the game is paused (IsTickableWhenPaused == true)
and not tickable in the editor (IsTickableInEditor == false).
Register any UObject that implements ISignalNexusInterceptor on a channel. Matching
is hierarchical: an interceptor on Gameplay.Damage also inspects
Gameplay.Damage.Physical. For each matching signal it may:
Pass to let it continue, orBlock to drop it entirely (subscribers never see it).Implement it in Blueprints (Intercept is a BlueprintNativeEvent) or in C++ via
USignalNexusLambdaInterceptor for lightweight code-side middleware.
FSignalNexusPayload stores a live copy of a struct tagged with its UScriptStruct.
Unpacking as the wrong type never reinterprets memory — it fails cleanly, returns
false, logs a LogSignalNexus warning and leaves the destination untouched. The
typed Subscribe<T> API silently ignores signals whose payload is not a T, so a
subscriber only ever sees the type it asked for.
Four ready-made payloads cover common cases with zero boilerplate:
FSignalIntPayload, FSignalNamePayload, FSignalVectorPayload,
FSignalDamagePayload. For anything else, just broadcast your own struct.
TMap<FGameplayTag, …> lookup per level of the channel's parent chain —Immediate path beyond the payload copy.Throttled mode coalesces bursts so extremely frequent producers (e.g. per-framePrivate/Tests/SignalNexusTests.cpp)UnrealEditor-Cmd.exe YourProject.uproject -ExecCmds="Automation RunTests SignalNexus; Quit" -unattended -nop4 -nosplash
| Symptom | Likely cause / fix |
|---|---|
Get() returns null |
Called before the GameInstance exists, or from an object with no world context. Call from/after BeginPlay. |
| Subscriber never fires | Channel mismatch (check the tag spelling), or the payload type doesn't match the subscribed T / unpacked struct. Use Get Payload Type Name to inspect. |
| Blueprint wildcard pin shows an error | Reconnect the struct to the Value/OutValue pin so the node can resolve the type. |
| Throttled signals feel laggy | ThrottleRate is dispatches per second — raise it for snappier updates. |
No LogSignalNexus output at all |
Module not loaded — confirm the plugin is enabled and the project rebuilt. |
© 2026 Silvan Teufel. All Rights Reserved.