One Handler, Many Devices: Event Wiring Patterns
In this lesson you'll learn to
- βStudent can wire an array of @editable triggers to a shared handler and identify which device/agent fired (fan-in event pattern).
Student can wire an array of @editable triggers to a shared handler and identify which device/agent fired (fan-in event pattern).
π Reference & full walkthrough: One Handler, Many Devices: Event Wiring Patterns
π§© Your capstone piece: Wiring all ~10 shell triggers to a single CollectShell handler instead of 10 copies
Overview
In UEFN, devices communicate through events β signals that broadcast whenever something notable happens. A trigger_device fires TriggeredEvent when a player walks over it. A button_device fires InteractedWithEvent when a player holds the interact key long enough. Your Verse code subscribes a handler function to these events, and that function is called automatically every time the event fires.
The core pattern is always the same:
Device.SomeEvent.Subscribe(MyHandlerMethod)
The handler method must match the event's payload type exactly:
listenable(agent)β your handler takes(Agent : agent)listenable(?agent)β your handler takes(Agent : ?agent)(optional β must be unwrapped)listenable(tuple())β your handler takes(Empty : tuple())
Subscribe is the glue between the visual devices you place in the editor and the logic you write in Verse. Without it, your devices are inert props. With it, they become the nervous system of your game.
API Reference
trigger_device
Used to relay events to other linked devices.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse). Inherited members are merged from trigger_base_device.
trigger_device<public> := class<concrete><final>(trigger_base_device):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
TriggeredEvent |
TriggeredEvent<public>:listenable(?agent) |
Signaled when an agent triggers this device. Sends the agent that used this device. Returns false if no agent triggered the action (ex: it was triggered through code). |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
Trigger |
Trigger<public>(Agent:agent):void |
Triggers this device with Agent being passed as the agent that triggered the action. Use an agent reference when this device is setup to require one (for instance, you want to trigger the device only with a particular agent. |
Trigger |
Trigger<public>():void |
Triggers this device, causing it to activate its TriggeredEvent event. |
Enable |
Enable<public>():void |
Enables this device. |
Disable |
Disable<public>():void |
Disables this device. |
SetMaxTriggerCount |
SetMaxTriggerCount<public>(MaxCount:int):void |
Sets the maximum amount of times this device can trigger. * 0 can be used to indicate no limit on trigger count. * MaxCount is clamped between [0,20]. |
GetMaxTriggerCount |
GetMaxTriggerCount<public>()<transacts>:int |
Gets the maximum amount of times this device can trigger. * 0 indicates no limit on trigger count. |
GetTriggerCountRemaining |
GetTriggerCountRemaining<public>()<transacts>:int |
Returns the number of times that this device can still be triggered before hitting GetMaxTriggerCount. Returns 0 if GetMaxTriggerCount is unlimited. |
SetResetDelay |
SetResetDelay<public>(Time:float):void |
Sets the time (in seconds) after triggering, before the device can be triggered again (if MaxTrigger count allows). |
GetResetDelay |
GetResetDelay<public>()<transacts>:float |
Gets the time (in seconds) before the device can be triggered again (if MaxTrigger count allows). |
SetTransmitDelay |
SetTransmitDelay<public>(Time:float):void |
Sets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered. |
GetTransmitDelay |
GetTransmitDelay<public>()<transacts>:float |
Gets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered. |
button_device
Used to create a button which can trigger other devices when an agent interacts with it.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
button_device<public> := class<concrete><final>(creative_device_base):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
InteractedWithEvent |
InteractedWithEvent<public>:listenable(agent) |
Signaled when an agent successfully interacts with the button for GetInteractionTime seconds. Sends the agent that interacted with the button. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
Enable |
Enable<public>():void |
Enables this device. |
Disable |
Disable<public>():void |
Disables this device. |
SetInteractionText |
SetInteractionText<public>(Text:message):void |
Sets the text that displays when an agent is close to this button and looks at it. Text is limited to 64 characters. |
SetInteractionTime |
SetInteractionTime<public>(Time:float):void |
Sets the duration of the interaction required to activate this device (in seconds). |
GetInteractionTime |
GetInteractionTime<public>()<transacts>:float |
Returns the duration of the interaction required to activate this device (in seconds). |
SetMaxTriggerCount |
SetMaxTriggerCount<public>(MaxCount:int):void |
Sets the maximum amount of times this button can be interacted with before it will be disabled. * MaxCount must be between 0 and 10000. * 0 indicates no limit on trigger count. |
GetMaxTriggerCount |
GetMaxTriggerCount<public>()<transacts>:int |
Returns the maximum amount of times this button can be interacted with before it will be disabled. * GetTriggerMaxCount will be between 0 and 10000. * 0 indicates no limit on trigger count. |
GetTriggerCountRemaining |
GetTriggerCountRemaining<public>()<transacts>:int |
Returns the number of times that this button can still be interacted with before it will be disabled. Will return 0 if GetMaxTriggerCount is unlimited. |
player
Full public surface, resolved verbatim from the live Epic digest (Verse.digest.verse). Inherited members are merged from agent.
player<native><public> := class<unique><persistent><module_scoped_var_weak_map_key><epic_internal>(agent):
Walkthrough
Scene: A 2D cel-shaded pirate cove at golden hour. A rickety dock juts into turquoise water. At the end of the dock sits a weathered button labeled "Raise Bridge". Inland, a pressure-plate trigger is hidden under a pile of rope β step on it and a cannon fires a cinematic sequence. Your Verse device wires both up.
Here is the complete, compilable device:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# dock_controller β wires a button and a trigger on a pirate dock
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
dock_controller := class(creative_device):
# ββ Editable references β drag your placed devices here ββ
@editable
BridgeButton : button_device = button_device{}
@editable
CannonTrigger : trigger_device = trigger_device{}
@editable
CannonCinematic : cinematic_sequence_device = cinematic_sequence_device{}
# ββ Localized text helper for SetInteractionText ββ
BridgePrompt<localizes>(S : string) : message = "{S}"
# ββ Entry point ββββββββββββββββββββββββββββββββββββββββββ
OnBegin<override>()<suspends> : void =
# 1. Customize the button prompt the player sees on the dock
BridgeButton.SetInteractionText(BridgePrompt("Raise the Bridge"))
# 2. Require the player to hold for 1.5 seconds (adds tension!)
BridgeButton.SetInteractionTime(1.5)
# 3. The bridge can only be raised once per round
BridgeButton.SetMaxTriggerCount(1)
# 4. The cannon trigger resets after 5 seconds so it can fire again
CannonTrigger.SetResetDelay(5.0)
# 5. Subscribe handlers β this is the magic line
BridgeButton.InteractedWithEvent.Subscribe(OnBridgeButtonPressed)
CannonTrigger.TriggeredEvent.Subscribe(OnCannonTriggered)
# ββ Handler: button_device sends a plain `agent` βββββββββ
OnBridgeButtonPressed(Agent : agent) : void =
# Fire the trigger device to relay the event to any linked
# devices (e.g. a mover that lifts the drawbridge prop)
CannonTrigger.Trigger(Agent)
# ββ Handler: trigger_device sends `?agent` (optional!) βββ
OnCannonTriggered(MaybeAgent : ?agent) : void =
# Unwrap the optional before using it
if (A := MaybeAgent?):
# A real player stepped on the plate β play cinematic for them
CannonCinematic.Play(A)
else:
# Triggered through code (no player) β play for everyone
CannonCinematic.Play()
Line-by-line explanation
| Lines | What's happening |
|---|---|
@editable fields |
Declare references to placed devices. Without @editable, Verse can't see the devices you dropped in the editor. |
BridgePrompt<localizes> |
SetInteractionText takes a message, not a raw string. This helper converts a string literal into a localized message value. |
SetInteractionTime(1.5) |
Players must hold the interact key for 1.5 s β great for high-stakes moments on a pirate dock. |
SetMaxTriggerCount(1) |
The bridge only goes up once. After that the button auto-disables. |
SetResetDelay(5.0) |
The cannon trigger won't fire again for 5 seconds after each activation. |
.Subscribe(OnBridgeButtonPressed) |
Registers the method as a callback. Verse calls it every time a player successfully interacts with the button. |
.Subscribe(OnCannonTriggered) |
TriggeredEvent is listenable(?agent) β the handler receives ?agent, not agent. |
if (A := MaybeAgent?) |
The canonical way to unwrap an optional ?agent. If no agent triggered it (e.g. code called Trigger()), the else branch runs. |
CannonCinematic.Play(A) |
Plays the cinematic sequence with the triggering player as the instigator. |
Common patterns
Pattern 1 β Disable a trigger after first use (one-shot trap)
A spike trap on the dock should only catch the first pirate who steps on it, then go dormant.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
spike_trap_controller := class(creative_device):
@editable
SpikeTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
# Allow exactly one activation, then the trigger disables itself
SpikeTrigger.SetMaxTriggerCount(1)
SpikeTrigger.TriggeredEvent.Subscribe(OnSpiked)
OnSpiked(MaybeAgent : ?agent) : void =
# After the first victim, explicitly disable so no edge cases slip through
SpikeTrigger.Disable()
# Optionally re-enable after a delay by calling SpikeTrigger.Enable() elsewhere
Key calls: SetMaxTriggerCount(1) caps activations; Disable() locks it down immediately in the handler.
Pattern 2 β Dynamic button prompt that changes after use
A treasure chest button on the ship's deck shows "Open Chest" before use and is reconfigured after the player interacts.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
treasure_chest_controller := class(creative_device):
@editable
ChestButton : button_device = button_device{}
@editable
TreasureTrigger : trigger_device = trigger_device{}
OpenPrompt<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends> : void =
ChestButton.SetInteractionText(OpenPrompt("Open Chest"))
ChestButton.SetInteractionTime(0.5)
# Only one treasure per chest!
ChestButton.SetMaxTriggerCount(1)
ChestButton.InteractedWithEvent.Subscribe(OnChestOpened)
OnChestOpened(Agent : agent) : void =
# Relay to a trigger that's linked to an item granter
TreasureTrigger.Trigger(Agent)
# Disable the button so the prompt disappears
ChestButton.Disable()
Key calls: SetInteractionText with a localized message, SetInteractionTime, Trigger(Agent) to relay the agent downstream, Disable() to hide the prompt.
Pattern 3 β Transmit delay for a cinematic beat
A lighthouse trigger on the clifftop fires a cinematic, but you want the trigger to wait 2 seconds before telling linked devices (like a score manager) that it fired β giving the cutscene a moment to breathe.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
lighthouse_controller := class(creative_device):
@editable
LighthouseTrigger : trigger_device = trigger_device{}
@editable
LighthouseCinematic : cinematic_sequence_device = cinematic_sequence_device{}
OnBegin<override>()<suspends> : void =
# Delay outbound signal to linked devices by 2 seconds
LighthouseTrigger.SetTransmitDelay(2.0)
# Confirm the delay was applied (useful for debugging)
var Delay : float = LighthouseTrigger.GetTransmitDelay()
LighthouseTrigger.TriggeredEvent.Subscribe(OnLighthouseActivated)
OnLighthouseActivated(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
LighthouseCinematic.Play(A)
else:
LighthouseCinematic.Play()
Key calls: SetTransmitDelay(2.0) staggers the outbound signal; GetTransmitDelay() reads it back for verification.
Gotchas
1. ?agent vs agent β know your event payload
trigger_device.TriggeredEvent is listenable(?agent) β the agent is optional because the trigger can be fired from code with no player. Always unwrap with if (A := MaybeAgent?): before using the agent. button_device.InteractedWithEvent is listenable(agent) β a button always has a real player, so no unwrap needed.
2. @editable is mandatory
You cannot write trigger_device{}.TriggeredEvent.Subscribe(...) and expect it to refer to a device in your level. You must declare the field as @editable inside your creative_device class and then assign the placed device in the UEFN Details panel. A bare constructor creates a disconnected in-memory object.
3. message is not a string
button_device.SetInteractionText takes a message, not a string. Passing a raw string literal will fail to compile. Declare a <localizes> helper method:
MyLabel<localizes>(S : string) : message = "{S}"
Then call SetInteractionText(MyLabel("My Text")). There is no StringToMessage function.
4. Subscribe in OnBegin, not at field initialisation
Subscriptions must happen at runtime inside OnBegin. You cannot subscribe at class-field level because the devices haven't been bound to their editor instances yet.
5. SetMaxTriggerCount clamps differently per device
trigger_device: clamped to[0, 20].0= unlimited.button_device: clamped to[0, 10000].0= unlimited. Passing a value outside the range won't error β it silently clamps. Always checkGetMaxTriggerCount()after setting if you need to verify.
6. Disable() vs SetMaxTriggerCount(1)
Both can make a device fire only once, but they behave differently. SetMaxTriggerCount(1) lets the device fire once then auto-disables. Disable() stops it immediately regardless of count. Use SetMaxTriggerCount when you want the device to handle its own lifecycle; use Disable() when your Verse logic needs direct control.
7. Transactions and Subscribe
If a transaction containing a Subscribe() call rolls back (e.g. inside a failing if block that uses <transacts>), the subscription is never registered. Make sure your Subscribe calls are in unconditional code paths inside OnBegin.
Keep going β free
You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.
Sign in free to continue this lessonSources
/guides/subscribe-to-eventBiloxi Studios original lesson
Add this lesson to your free study plan.
π§ The Keeperβs log
Quest complete? Chart your next heading from the 𦩠South Shores (Free) expedition.
β΅ Take me there