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 check GetMaxTriggerCount() 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 lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/subscribe-to-event
Source

Biloxi Studios original lesson

Guided course

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