Module: CrowPuzzleSystem | Engine: Unreal Engine 5.6 | © 2026 SuspectCrow
1. Overview
The Crow Puzzle System is a signal-routing graph framework for Unreal Engine 5. Actors equipped with USCPuzzleNodeComponent form a directed graph: each node receives a typed signal, filters and transforms it through an ordered rule pipeline, and — if all rules pass — fires a local delegate and forwards the resulting signal to its connected downstream nodes.
The entire puzzle topology is configured in the editor Details panel with zero per-puzzle C++ code.
Design Goals
| Goal | How the System Achieves It |
|---|---|
| No per-puzzle code | Topology (which node connects to which) is set via TargetNodes in the Details panel. Logic lives in instanced USCPuzzleRule objects on each node |
| Typed signals | FSCPuzzlePayload carries a FGameplayTag for identity, a FInstancedStruct for arbitrary typed data, and a weak source reference |
| Composable logic | Rules run as an ordered pipeline. Each rule receives the previous rule’s output, so rules can filter, transform, or pass signals through in sequence |
| Extensible rules | USCPuzzleRule is abstract and EditInlineNew. Subclass it in C++ or Blueprint to add any filtering or transformation logic |
| Dual-target routing | Downstream nodes can be any actor that either directly implements ISCPuzzleNodeInterface or owns a component that does |
2. Architecture
The system is a directed acyclic graph (though the engine imposes no loop detection). Each node in the graph is a USCPuzzleNodeComponent attached to a world actor.
Source Actor Intermediate Actor Target Actor
┌──────────────┐ ┌──────────────────┐ ┌─────────────┐
│ (e.g. Button)│ │ (e.g. Gate) │ │ (e.g. Door) │
│ │ │ │ │ │
│ USCPuzzle │ Signal │ USCPuzzleNode │ Signal │ USCPuzzle │
│ NodeComponent│ ─────────► │ Component │ ──────────► │ NodeComp. │
│ │ │ │ │ │
│ TargetNodes │ │ ValidationRules: │ │ (no rules, │
│ └─► Gate │ │ [0] ExcludeTag │ │ triggers │
└──────────────┘ │ [1] ConvertTag │ │ animation) │
│ │ └─────────────┘
│ TargetNodes: │
│ └─► Door │
└──────────────────┘
Signal Flow Inside a Single Node
ReceivePuzzleSignal(InPayload)
│
├─ Copy InPayload → CurrentPayload
│
├─ For each Rule in ValidationRules (in order):
│ Rule.EvaluateSignal(CurrentPayload, CurrentPayload)
│ │
│ ├─ returns false → bAllRulesPassed = false → BREAK
│ └─ returns true → CurrentPayload may have been mutated → continue
│
├─ bAllRulesPassed == false → STOP (signal consumed silently)
│
└─ bAllRulesPassed == true:
├─ OnPuzzleSignalReceived.Broadcast(CurrentPayload)
└─ For each actor in TargetNodes:
├─ Actor implements ISCPuzzleNodeInterface directly?
│ YES → Execute_ReceivePuzzleSignal(Actor, CurrentPayload)
│ NO → GetComponentsByInterface(USCPuzzleNodeInterface)
│ → Execute_ReceivePuzzleSignal(Comp, CurrentPayload)
└─ (next target)
3. Quick Start
Step 1 — Add USCPuzzleNodeComponent to a Puzzle Actor
Any actor that participates in the puzzle graph needs this component. Add it in the Blueprint editor or in C++:
// MyPuzzleButton.h #include "Components/SCPuzzleNodeComponent.h" UPROPERTY(VisibleAnywhere, BlueprintReadOnly) TObjectPtr<USCPuzzleNodeComponent> PuzzleNode;
// MyPuzzleButton.cpp — constructor
PuzzleNode = CreateDefaultSubobject<USCPuzzleNodeComponent>(TEXT("PuzzleNode"));
Step 2 — Send a Signal from a Source Actor
When a gameplay event occurs (button pressed, trigger entered, lever pulled), construct a payload and call ReceivePuzzleSignal on the component:
// In a Blueprint or C++ interaction handler:
FSCPuzzlePayload Payload;
Payload.SignalTag = FGameplayTag::RequestGameplayTag(TEXT("SC.Puzzle.Signal.Activate"));
Payload.SourceObject = this; // optional weak reference to the sender
PuzzleNode->ReceivePuzzleSignal(Payload);
Blueprint: Construct a FSCPuzzlePayload struct, set its SignalTag, then call Receive Puzzle Signal on the component.
Step 3 — Wire Nodes Together in the Editor
On the source node’s Details panel, expand Target Nodes and add references to the downstream actors. The editor enforces that every entry implements ISCPuzzleNodeInterface, which USCPuzzleNodeComponent does automatically.
Important:
TargetNodesisEditInstanceOnly. You must wire connections on placed instances in the level, not on the Blueprint class default.
Step 4 — Add Rules to a Node (Optional)
On any intermediate node, expand Validation Rules in the Details panel and add entries from the built-in rule classes or your own subclasses:
| Rule | Effect |
|---|---|
Exclude Tag (Blocker) | Blocks the signal if its tag matches TagToExclude |
Convert Tag | Replaces the signal tag if it exactly matches TagToFind |
Rules run top-to-bottom. Reorder them by dragging within the array.
Step 5 — React to the Signal on the Destination Node
Bind to OnPuzzleSignalReceived in Blueprint:
BeginPlay (on the Door actor) └── PuzzleNodeComponent → Bind OnPuzzleSignalReceived → OnSignalReceived OnSignalReceived(Payload) └── If Payload.SignalTag == SC.Puzzle.Signal.Activate → Play Open Animation
4. Core Classes & Types
4.1 FSCPuzzlePayload
File: Structs/SCPuzzlePayload.h
The data packet that travels through the puzzle graph. Every node receives one, every rule inspects and optionally modifies one, and every downstream node receives the final transformed version.
| Property | Type | Description |
|---|---|---|
SignalTag | FGameplayTag | Identifies the signal’s type or intent. Rules and downstream nodes switch on this tag. A node may change this tag in transit (e.g., via ConvertTag). |
SignalData | FInstancedStruct | Arbitrary typed payload, powered by the UE5 StructUtils plugin. Allows any USTRUCT to be embedded without a fixed class dependency. Can be left empty for simple on/off signals. |
SourceObject | TWeakObjectPtr<UObject> | A weak reference to the object that originated the signal. Useful for downstream nodes that need to identify who sent the signal (e.g., which pressure plate was triggered). Safe to be null. |
Using SignalData
FInstancedStruct allows you to embed any USTRUCT into the payload at runtime:
// Sender side — embedding custom data:
USTRUCT(BlueprintType)
struct FMyDamageData { GENERATED_BODY() int32 DamageAmount = 0; };
FSCPuzzlePayload Payload;
Payload.SignalTag = FGameplayTag::RequestGameplayTag(TEXT("SC.Puzzle.Signal.Damage"));
Payload.SignalData.InitializeAs<FMyDamageData>();
Payload.SignalData.GetMutable<FMyDamageData>()->DamageAmount = 50;
// Receiver side — reading custom data:
if (const FMyDamageData* Data = InPayload.SignalData.GetPtr<FMyDamageData>())
{
TakeDamage(Data->DamageAmount);
}
For simple signals that carry no extra data (a button activation, a door trigger), leave SignalData default-constructed.
4.2 ISCPuzzleNodeInterface
File: Interfaces/SCPuzzleNodeInterface.h
The single-method interface that marks an actor or component as a puzzle graph node. Any participant in the graph must implement this interface, directly or through a component.
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Puzzle") void ReceivePuzzleSignal(const FSCPuzzlePayload& InPayload);
USCPuzzleNodeComponent provides a full C++ implementation of this method. For most actors, simply adding the component is sufficient — you do not need to also implement the interface on the actor class itself.
4.3 USCPuzzleNodeComponent
File: Components/SCPuzzleNodeComponent.h | Base: UActorComponent
The core of the system. This component is both a node in the graph (it implements ISCPuzzleNodeInterface) and a router (it forwards processed signals to TargetNodes).
Tick is enabled by default (
bCanEverTick = true). Disable it on instances that do not need per-frame logic.
Properties
| Property | Editor Visibility | Description |
|---|---|---|
ValidationRules | EditAnywhere (Instanced) | Ordered array of USCPuzzleRule objects. Evaluated top-to-bottom. Each rule receives the output of the previous one. All rules must pass for the signal to continue. |
TargetNodes | EditInstanceOnly | Actors to forward the signal to after all rules pass. Every entry must implement ISCPuzzleNodeInterface (enforced by MustImplement meta). Set per-level-instance, not on the class default. |
Delegate
| Delegate | Signature | Fired When |
|---|---|---|
OnPuzzleSignalReceived | (const FSCPuzzlePayload& Payload) | All ValidationRules returned true. Fires before forwarding to TargetNodes, so the local actor can react first. |
4.4 USCPuzzleRule
File: Objects/SCPuzzleRule.h | Base: UObject
Abstract base class for all puzzle rules. Rules are the logic atoms of the system — each one has a single job: inspect the current payload, optionally modify it, and return whether the signal should continue.
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Puzzle|Rule") bool EvaluateSignal(const FSCPuzzlePayload& InPayload, FSCPuzzlePayload& OutPayload) const;
| Parameter | Direction | Description |
|---|---|---|
InPayload | In | The payload as received by this rule (output of the previous rule in the chain). |
OutPayload | Out | The payload this rule passes forward. Typically start with OutPayload = InPayload and then modify only what you need. |
| Return value | — | true = signal passes through; false = signal is blocked here, chain stops. |
Because rules are EditInlineNew and DefaultToInstanced, each rule is configured and stored directly inside the component’s ValidationRules array — no separate asset is needed.
4.5 USCPuzzleRule_ConvertTag
File: Rules/SCPuzzleRule_ConvertTag.h | Display Name: Convert Tag
Transforms the signal’s SignalTag when it matches a specific tag. The signal always passes through (returns true); this rule only mutates, never blocks.
| Property | Type | Description |
|---|---|---|
TagToFind | FGameplayTag | The tag to look for in the incoming payload. Matching is exact — child tags do not trigger the conversion. |
TagToReplaceWith | FGameplayTag | The tag written to OutPayload.SignalTag when a match is found. |
Example Use Case
A lever sends SC.Puzzle.Signal.LeverPulled. A downstream gate node uses a ConvertTag rule to translate it into SC.Puzzle.Signal.Activate before forwarding, so the door at the end of the chain only needs to understand one standardized signal.
4.6 USCPuzzleRule_ExcludeTag
File: Rules/SCPuzzleRule_ExcludeTag.h | Display Name: Exclude Tag (Blocker)
Blocks any signal whose tag matches TagToExclude. Acts as a gate that rejects specific signal types.
| Property | Type | Description |
|---|---|---|
TagToExclude | FGameplayTag | The tag to block. Matching is hierarchical — if TagToExclude is SC.Puzzle.Signal.Damage, then a signal tagged SC.Puzzle.Signal.Damage.Fire is also blocked. |
bAltTagsIncluded | bool | Reserved for Blueprint subclass customization or future expansion. The built-in C++ implementation does not read this value. |
Example Use Case
A puzzle has two input sources: a friendly button and a hostile explosion. The door should only respond to the button. Add an ExcludeTag rule on the door’s node to block explosion tags.
5. Signal Processing Pipeline
Chained Rule Evaluation
Rules are evaluated in array order. The key implementation detail is that the same CurrentPayload variable is used for both InPayload and OutPayload on every rule call:
// From USCPuzzleNodeComponent::ReceivePuzzleSignal_Implementation:
FSCPuzzlePayload CurrentPayload = InPayload;
for (const TObjectPtr<USCPuzzleRule>& Rule : ValidationRules)
{
if (Rule)
{
if (!Rule->EvaluateSignal(CurrentPayload, CurrentPayload))
{
bAllRulesPassed = false;
break;
}
}
}
This means each rule’s output becomes the next rule’s input. A ConvertTag rule placed before an ExcludeTag rule will operate on the converted tag, not the original one.
Rule Order Matters
Signal in: SC.Puzzle.Signal.LeverPulled
Order A — Convert first, then Exclude:
[0] ConvertTag: LeverPulled → Activate
[1] ExcludeTag: Activate
→ Signal is blocked (ExcludeTag sees "Activate")
Order B — Exclude first, then Convert:
[0] ExcludeTag: Activate
[1] ConvertTag: LeverPulled → Activate
→ Signal passes and is forwarded as "Activate"
(ExcludeTag sees "LeverPulled", which doesn't match "Activate")
6. Node Routing
After all rules pass, the component forwards the final CurrentPayload to every actor in TargetNodes using a two-step resolution strategy:
For each TargetActor in TargetNodes:
Step 1 — Does the actor directly implement ISCPuzzleNodeInterface?
YES → ISCPuzzleNodeInterface::Execute_ReceivePuzzleSignal(TargetActor, Payload)
(works for actors with a custom C++ or Blueprint interface implementation)
Step 2 (fallback) — Find components that implement ISCPuzzleNodeInterface:
GetComponentsByInterface(USCPuzzleNodeInterface)
→ For each matching component:
ISCPuzzleNodeInterface::Execute_ReceivePuzzleSignal(Component, Payload)
(works for actors that have a USCPuzzleNodeComponent attached)
| Actor setup | Resolution path used |
|---|---|
Actor has USCPuzzleNodeComponent | Step 2 — component is found and signaled |
| Actor implements interface directly in C++ or Blueprint | Step 1 — actor is signaled directly |
| Actor has both | Step 1 takes priority |
Important:
TargetNodesisEditInstanceOnly. You must set up connections on the actor instance placed in the level, not on the Blueprint class default. This is intentional — puzzle wiring is level-specific, not class-specific.
7. Creating Custom Rules
In Blueprint
- Create a Blueprint with parent class
USCPuzzleRule - Override the
Evaluate Signalevent - Set
OutPayload, then returntrue(pass) orfalse(block) - Add it to a node’s
Validation Rulesarray in the Details panel
In C++
UCLASS(meta = (DisplayName = "Require Source Object"))
class UMyRule_RequireSource : public USCPuzzleRule
{
GENERATED_BODY()
public:
virtual bool EvaluateSignal_Implementation(
const FSCPuzzlePayload& InPayload,
FSCPuzzlePayload& OutPayload) const override
{
OutPayload = InPayload;
// Block the signal if no source object is attached
if (!InPayload.SourceObject.IsValid())
{
return false;
}
return true;
}
};
Rule Design Principles
Always start your implementation with OutPayload = InPayload. This ensures that any properties you do not explicitly change pass through unchanged.
A rule should do exactly one thing. Compose complex behavior by stacking multiple simple rules rather than writing one monolithic rule. This makes the logic visible and reorderable in the editor.
Return false only when you have a definitive reason to block the signal. Rules that return false silently consume the signal — nothing downstream fires and no error is shown.
8. Common Patterns & Recipes
Pattern 1 — Simple Button → Door
[BP_PuzzleButton] [BP_PuzzleDoor] USCPuzzleNodeComponent USCPuzzleNodeComponent ValidationRules: (empty) ValidationRules: (empty) TargetNodes: → BP_PuzzleDoor OnPuzzleSignalReceived → PlayOpenAnimation
OnPlayerInteract
└── PuzzleNode → Receive Puzzle Signal
SignalTag: SC.Puzzle.Signal.Activate
SourceObject: Self
Pattern 2 — AND Gate (All Buttons Must Be Pressed)
[BP_PuzzleGate]
USCPuzzleNodeComponent
ValidationRules: (empty)
TargetNodes: → BP_PuzzleDoor
// Blueprint variables:
bButton1Active: bool = false
bButton2Active: bool = false
OnPuzzleSignalReceived(Payload)
├─ If Payload.SignalTag == SC.Puzzle.Signal.Button1 → bButton1Active = true
├─ If Payload.SignalTag == SC.Puzzle.Signal.Button2 → bButton2Active = true
└─ If bButton1Active AND bButton2Active:
Send SC.Puzzle.Signal.Activate → PuzzleNode.ReceivePuzzleSignal
Pattern 3 — Tag Converter as a Signal Adapter
[Combat System] [Converter Node] [Puzzle Door]
Sends: ValidationRules: Expects:
SC.Combat.Enemy.Defeated → ConvertTag: → SC.Puzzle.Signal.Activate
Find: SC.Combat.Enemy.Defeated
Replace: SC.Puzzle.Signal.Activate
TargetNodes: → PuzzleDoor
Pattern 4 — Blocked Channel (Exclude as a Switch)
[Hazard Trigger] → [Blocker Node] → [Trap]
ValidationRules:
ExcludeTag: SC.Puzzle.Signal.Hazard
[Safety Lever] → (disables the Blocker Node's component at runtime)
// At runtime from the lever's OnPuzzleSignalReceived: BlockerNode->PuzzleNodeComponent->SetActive(false); // Now signals flow through unimpeded
Pattern 5 — Multi-Tag Dispatch (One Source, Many Destinations)
[Central Relay Node] ValidationRules: (empty) TargetNodes: → BP_Door_A → BP_TrapSpawner → BP_AmbientLight
Pattern 6 — Sequential Puzzle with ConvertTag Chaining
Step 1: Pressure plate sends SC.Puzzle.Signal.Step1Done
└─► Gate node
ConvertTag: Step1Done → Step2Unlock
TargetNodes: → [Torch Holder]
Step 2: Torch Holder expects SC.Puzzle.Signal.Step2Unlock to enable itself
Player places torch → sends SC.Puzzle.Signal.Step2Done
└─► Gate node
ConvertTag: Step2Done → Activate
TargetNodes: → [Final Door]
Pattern 7 — Forwarding SignalData Through the Graph
// Trap actor — when triggered:
FSCPuzzlePayload Payload;
Payload.SignalTag = FGameplayTag::RequestGameplayTag(TEXT("SC.Puzzle.Signal.Damage"));
Payload.SignalData.InitializeAs<FMyDamageData>();
Payload.SignalData.GetMutable<FMyDamageData>()->DamageAmount = 75;
Payload.SourceObject = this;
PuzzleNode->ReceivePuzzleSignal(Payload);
// Health component node — OnPuzzleSignalReceived:
void AMyHealthActor::OnSignalReceived(const FSCPuzzlePayload& Payload)
{
if (const FMyDamageData* Data = Payload.SignalData.GetPtr<FMyDamageData>())
{
CurrentHealth -= Data->DamageAmount;
}
}
Pattern 8 — Custom Rule: Require Specific Source Actor Class
UCLASS(meta = (DisplayName = "Require Source Class"))
class UMyRule_RequireClass : public USCPuzzleRule
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "SC|Puzzle|Rule")
TSubclassOf<AActor> RequiredClass;
virtual bool EvaluateSignal_Implementation(
const FSCPuzzlePayload& InPayload,
FSCPuzzlePayload& OutPayload) const override
{
OutPayload = InPayload;
if (!RequiredClass) return true;
const AActor* Source = Cast<AActor>(InPayload.SourceObject.Get());
return Source && Source->IsA(RequiredClass);
}
};
EN
TR