Overview
Every non-trivial Creative map eventually hits the same problem: you need one event to trigger many things, or you need a single named signal that multiple Verse devices can both fire and listen to. Wiring every source directly to every destination creates a spaghetti of connections that breaks the moment you add a new device.
channel_device solves this cleanly:
- Any Verse code (or another device in the editor) calls
Transmit(Agent)to broadcast a signal. - Every subscriber of
ReceivedTransmitEventreacts immediately — whether that's unlocking a vault door, spawning a VFX burst, or updating a score tracker. - Because the channel is a single named device you drop on the island, you can wire dozens of sources and dozens of listeners through it without touching the wiring of any individual device.
Reach for channel_device when:
- You want a named, reusable signal bus (e.g.
RoundEndChannel,BossDefeatedChannel). - Multiple independent Verse devices need to fire the same downstream logic.
- You want editor-wired devices (buttons, triggers, etc.) to talk to Verse code through a clean handoff point.
- You need to pass an optional
agentinstigator along with the signal.
API Reference
channel_device
Sends an Event for every signal it receives. A simple relay to connect multiple devices together. It can also broadcast an event to the playspace and listen for any other channel devices broadcasting to the playspace.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
channel_device<public> := class<concrete><final>(creative_device_base):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
ReceivedTransmitEvent |
ReceivedTransmitEvent<public>:listenable(?agent) |
Signaled when either Transmit is called. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
Transmit |
Transmit<public>(Agent:?agent):void |
Calls ReceivedTransmitEvent, optionally with Agent as the instigator. |
Walkthrough
Scenario: Vault Door That Opens When All Three Plates Are Stepped On
You have a vault room protected by three pressure plates. Only when all three are active should the vault door open. A channel_device acts as the "all clear" signal: your Verse manager listens to each plate, and when the final condition is met it calls Channel.Transmit(Agent). A second subscription on the same channel drives the door-open VFX spawner.
Island setup:
- Three
trigger_deviceinstances (Plate1, Plate2, Plate3) — set to "Player steps on" mode. - One
channel_device(VaultOpenChannel) — the shared signal bus. - One
vfx_spawner_device(VaultVFX) — plays a burst when the vault opens. - One
channel_device(VaultVFXChannel) — wired in the editor to enable VaultVFX (optional editor wiring; the Verse device also calls it directly).
vault_manager := class(creative_device):
# The three pressure plates in the vault room
@editable
Plate1 : trigger_device = trigger_device{}
@editable
Plate2 : trigger_device = trigger_device{}
@editable
Plate3 : trigger_device = trigger_device{}
# The channel that signals "vault is open" to all listeners
@editable
VaultOpenChannel : channel_device = channel_device{}
# VFX spawner that plays the vault-open burst
@editable
VaultVFX : vfx_spawner_device = vfx_spawner_device{}
# Track which plates are active
var Plate1Active : logic = false
var Plate2Active : logic = false
var Plate3Active : logic = false
OnBegin<override>()<suspends> : void =
# Subscribe to each plate's triggered event
Plate1.TriggeredEvent.Subscribe(OnPlate1Stepped)
Plate2.TriggeredEvent.Subscribe(OnPlate2Stepped)
Plate3.TriggeredEvent.Subscribe(OnPlate3Stepped)
# Subscribe to the vault channel so we can trigger the VFX
VaultOpenChannel.ReceivedTransmitEvent.Subscribe(OnVaultOpened)
# Plate handlers — each marks its plate active, then checks all three
OnPlate1Stepped(Agent : ?agent) : void =
set Plate1Active = true
CheckAllPlates(Agent)
OnPlate2Stepped(Agent : ?agent) : void =
set Plate2Active = true
CheckAllPlates(Agent)
OnPlate3Stepped(Agent : ?agent) : void =
set Plate3Active = true
CheckAllPlates(Agent)
# Only transmit once all three plates are active
CheckAllPlates(Agent : ?agent) : void =
if (Plate1Active?, Plate2Active?, Plate3Active?):
# Broadcast the vault-open signal with the triggering agent
VaultOpenChannel.Transmit(Agent)
# React to the vault-open signal — enable the VFX burst
OnVaultOpened(Agent : ?agent) : void =
VaultVFX.Enable()
VaultVFX.Restart()
# Optionally do something with the instigating agent
if (A := Agent?):
# A is a valid agent — could grant a reward, update a score, etc.
VaultVFX.Enable()
Line-by-line breakdown:
| Lines | What's happening |
|---|---|
@editable fields |
Expose all devices to the UEFN editor so you can drag-assign them |
Plate1.TriggeredEvent.Subscribe(OnPlate1Stepped) |
Each plate's event feeds its own handler |
OnPlate1Stepped(Agent : ?agent) |
The handler signature matches listenable(?agent) — the agent is optional |
CheckAllPlates |
Central gate — only fires the channel when all three conditions are true |
VaultOpenChannel.Transmit(Agent) |
Broadcasts the signal; every subscriber of ReceivedTransmitEvent fires |
VaultOpenChannel.ReceivedTransmitEvent.Subscribe(OnVaultOpened) |
This device also listens to its own channel — valid and useful for decoupled logic |
if (A := Agent?) |
Safe unwrap of the optional agent before use |
Common patterns
Pattern 1 — Calling Transmit to fire a named game event
Use Transmit as a fire-and-forget broadcast. Here a button press signals a round-end channel that other systems (scoreboard, barrier, timer) can all subscribe to independently.
round_end_broadcaster := class(creative_device):
@editable
EndButton : button_device = button_device{}
# Shared signal bus — other devices subscribe to this in the editor or via Verse
@editable
RoundEndChannel : channel_device = channel_device{}
OnBegin<override>()<suspends> : void =
EndButton.InteractedWithEvent.Subscribe(OnEndButtonPressed)
OnEndButtonPressed(Agent : agent) : void =
# Wrap the concrete agent in an optional before passing to Transmit
RoundEndChannel.Transmit(option{Agent})
Key point: Transmit takes ?agent (an optional agent). When you have a concrete agent, wrap it with option{Agent} to match the signature. Pass false (the zero value for ?agent) when there is no meaningful instigator.
Pattern 2 — Subscribing to ReceivedTransmitEvent to react to an editor-wired signal
Here the channel is wired entirely in the editor (e.g. a Button device's "On Interact" output feeds the channel). The Verse device just listens and responds — no Transmit call needed from code.
boss_defeat_listener := class(creative_device):
# This channel is wired in the editor: BossHealthBar "On Zero" -> BossDefeatedChannel
@editable
BossDefeatedChannel : channel_device = channel_device{}
@editable
RewardVFX : vfx_spawner_device = vfx_spawner_device{}
OnBegin<override>()<suspends> : void =
BossDefeatedChannel.ReceivedTransmitEvent.Subscribe(OnBossDefeated)
OnBossDefeated(Agent : ?agent) : void =
# Enable and burst the reward VFX for everyone
RewardVFX.Enable()
RewardVFX.Restart()
# If we care who landed the killing blow, unwrap the agent
if (Killer := Agent?):
# Killer is now a valid agent — grant XP, show callout, etc.
RewardVFX.Restart()
Key point: ReceivedTransmitEvent is a listenable(?agent), so the handler always receives ?agent. Always unwrap with if (A := Agent?): before treating it as a concrete agent.
Pattern 3 — Using a channel as a two-way handshake between two Verse devices
Two separate Verse devices share one channel_device. Device A transmits; Device B listens and then transmits back on a second channel; Device A listens to that. This creates a clean request/acknowledge pattern.
lock_controller := class(creative_device):
@editable
UnlockRequestChannel : channel_device = channel_device{}
@editable
UnlockGrantedChannel : channel_device = channel_device{}
@editable
DoorVFX : vfx_spawner_device = vfx_spawner_device{}
OnBegin<override>()<suspends> : void =
# Listen for an unlock request from any source
UnlockRequestChannel.ReceivedTransmitEvent.Subscribe(OnUnlockRequested)
# Listen for the grant acknowledgement
UnlockGrantedChannel.ReceivedTransmitEvent.Subscribe(OnUnlockGranted)
OnUnlockRequested(Agent : ?agent) : void =
# Validate and then grant — here we always grant for simplicity
UnlockGrantedChannel.Transmit(Agent)
OnUnlockGranted(Agent : ?agent) : void =
DoorVFX.Enable()
DoorVFX.Restart()
Key point: A device can both Transmit on a channel and subscribe to ReceivedTransmitEvent on the same or a different channel. This makes channel_device a natural building block for event-driven state machines.
Gotchas
1. Transmit takes ?agent, not agent
The signature is Transmit(Agent : ?agent). If you have a concrete agent (e.g. from a button's InteractedWithEvent), you must wrap it: Channel.Transmit(option{Agent}). Passing a bare agent is a type error. To signal with no instigator, pass false — the zero/empty value for an optional in Verse.
2. Always unwrap ?agent before use
ReceivedTransmitEvent delivers ?agent. Accessing agent methods directly on an optional is a compile error. Always guard with:
if (A := Agent?):
# A is agent here
Skipping this is the single most common runtime-logic bug with channel events.
3. @editable is mandatory for placed devices
You cannot reference a placed channel_device with a bare identifier. It must be declared as an @editable field inside your creative_device class and assigned in the UEFN editor. A bare MyChannel.Transmit(...) with no field declaration will fail with Unknown identifier.
4. Subscribe in OnBegin, not at field initialisation
Event subscriptions must happen at runtime inside OnBegin<override>()<suspends>. Subscribing at the class field level (outside a method) is not valid Verse. If your handler never fires, check that Subscribe is called inside OnBegin.
5. channel_device is <final> — you cannot subclass it
The class is marked <concrete><final>, meaning you cannot extend it. If you need custom behaviour, wrap it: hold a channel_device as a field in your own creative_device and delegate to it.
6. One channel, many listeners — order is not guaranteed
All subscribers to ReceivedTransmitEvent fire when Transmit is called, but Verse does not guarantee the order in which concurrent subscribers execute. Do not write logic that depends on subscriber A completing before subscriber B starts.