Overview
Fortnite's creative devices each expose events (things that happen) and methods (things you tell them to do). The "custom event signal" pattern is how you connect them in Verse: you subscribe to one device's event, then call methods on other devices inside the handler. This keeps each device doing one job and lets your Verse class act as the clean coordinator.
When should you reach for this pattern?
- A damage volume on a cove dock needs to start a countdown the moment a player wades in.
- A timer reaching zero should fire a cinematic and award score to whoever triggered it.
- A teleporter arrival should reset the whole sequence for the next player.
Because Verse subscriptions are set up once in OnBegin, the wiring is explicit, traceable, and easy to extend.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
The Scenario
Sunny clifftop cove. A glowing damage volume sits over a shallow tidal pool at the base of the cliffs — the Hazard Zone. When a player steps in:
- A timer starts counting down (30 seconds to escape).
- A cinematic sequence plays (cel-shaded wave crash).
- If the player escapes before time runs out, the
SuccessEventawards them score via a score manager. - If they fail, the cinematic stops and the sequence resets.
Every device is wired in one Verse class — no Verse spaghetti, no hidden channel links.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
# ─────────────────────────────────────────────────────────────────
# cove_hazard_coordinator
# Coordinates the tidal-pool hazard on the clifftop cove.
# Place this device in the level, then assign the three @editable
# devices in the Details panel.
# ─────────────────────────────────────────────────────────────────
cove_hazard_coordinator := class(creative_device):
# The glowing tidal-pool damage volume.
@editable
HazardVolume : damage_volume_device = damage_volume_device{}
# 30-second countdown timer (configure duration in device settings).
@editable
EscapeTimer : timer_device = timer_device{}
# Cel-shaded wave-crash cinematic sequence.
@editable
WaveCinematic : cinematic_sequence_device = cinematic_sequence_device{}
# ── Lifecycle ────────────────────────────────────────────────
OnBegin<override>()<suspends> : void =
# Wire the damage volume's enter/exit events.
HazardVolume.AgentEntersEvent.Subscribe(OnPlayerEntersHazard)
HazardVolume.AgentExitsEvent.Subscribe(OnPlayerExitsHazard)
# Wire the timer's success and failure events.
EscapeTimer.SuccessEvent.Subscribe(OnEscapeSuccess)
EscapeTimer.FailureEvent.Subscribe(OnEscapeFailed)
# ── Handlers ─────────────────────────────────────────────────
# Called when any agent steps into the tidal pool.
OnPlayerEntersHazard(Agent : agent) : void =
# Start the escape countdown for this specific agent.
EscapeTimer.Start(Agent)
# Play the wave-crash cinematic for everyone.
WaveCinematic.Play()
# Ramp up the damage volume to make the hazard feel urgent.
HazardVolume.SetDamage(15)
# Called when the agent leaves the damage volume.
OnPlayerExitsHazard(Agent : agent) : void =
# Stop the timer for this agent — they made it out.
EscapeTimer.Reset(Agent)
# Ease off the damage (another player might still be inside).
HazardVolume.SetDamage(5)
# Timer fires SuccessEvent — agent escaped in time.
# SuccessEvent sends ?agent, so we must unwrap.
OnEscapeSuccess(MaybeAgent : ?agent) : void =
# Freeze the cinematic on its final frame as a reward flourish.
WaveCinematic.GoToEndAndStop()
# Timer fires FailureEvent — agent was too slow.
OnEscapeFailed(MaybeAgent : ?agent) : void =
# Stop the cinematic and reset it for the next attempt.
WaveCinematic.Stop()```
### Line-by-line explanation
| Lines | What's happening |
|---|---|
| `@editable` fields | Each device is declared as an editable field so UEFN can inject the placed instance. Without this, `HazardVolume.AgentEntersEvent` would be an unknown identifier. |
| `OnBegin` subscriptions | All four `Subscribe` calls run once at game start, wiring every event to its handler. |
| `OnPlayerEntersHazard` | Receives a concrete `agent` (not optional) because `AgentEntersEvent` is `listenable(agent)`. We immediately call `EscapeTimer.Start(Agent)` — the agent-overload starts a per-player countdown. |
| `HazardVolume.SetDamage(15)` | Dynamically increases tick damage the moment someone enters, adding pressure. |
| `OnEscapeSuccess(MaybeAgent : ?agent)` | `SuccessEvent` is `listenable(?agent)` — the payload is optional. We unwrap with `if (A := MaybeAgent?):` before calling `ScoreManager.Activate(A)`. |
| `WaveCinematic.GoToEndAndStop()` | Snaps the sequence to its last frame — a cel-shaded victory pose. |
| `ScoreManager.Reset()` on failure | Clears any partial state so the next player starts fresh. |
## Common patterns
### Pattern 1 — Teleporter arrival resets the whole hazard sequence
A sun-bleached teleporter on the clifftop drops players straight into the cove. When they arrive, reset the timer and cinematic so each arrival is a clean slate.
```verse
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
cove_teleporter_reset := class(creative_device):
# The clifftop teleporter that drops players into the cove.
@editable
CoveTeleporter : teleporter_device = teleporter_device{}
# Timer to reset on arrival.
@editable
EscapeTimer : timer_device = timer_device{}
# Cinematic to rewind on arrival.
@editable
WaveCinematic : cinematic_sequence_device = cinematic_sequence_device{}
OnBegin<override>()<suspends> : void =
# TeleportedEvent fires after the agent emerges at the destination.
CoveTeleporter.TeleportedEvent.Subscribe(OnPlayerArrived)
OnPlayerArrived(Agent : agent) : void =
# Reset the timer for this specific arriving agent.
EscapeTimer.Reset(Agent)
# Rewind the cinematic so the wave crash plays fresh.
WaveCinematic.PlayReverse()
Pattern 2 — Score manager increment chain (collect cove pearls)
Scatter item spawners across the cove. Each time the score manager fires ScoreOutputEvent, increment the next award so collecting pearls becomes exponentially rewarding.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
cove_pearl_scorer := class(creative_device):
# Score manager configured to award 10 points base.
@editable
PearlScore : score_manager_device = score_manager_device{}
OnBegin<override>()<suspends> : void =
# Every time a score is awarded, bump the next award up by 1.
PearlScore.ScoreOutputEvent.Subscribe(OnPearlCollected)
# ScoreOutputEvent sends the agent who received points.
OnPearlCollected(Agent : agent) : void =
# Increment so the NEXT activation awards one more point.
# Collecting pearls in a streak is increasingly rewarding.
PearlScore.Increment(Agent)
Pattern 3 — Damage volume query + cinematic pause on player count
If more than one player is in the hazard zone at the same time, pause the cinematic (the wave hangs mid-crash, cel-shaded and eerie) until the zone clears.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
cove_crowd_pause := class(creative_device):
@editable
HazardVolume : damage_volume_device = damage_volume_device{}
@editable
WaveCinematic : cinematic_sequence_device = cinematic_sequence_device{}
OnBegin<override>()<suspends> : void =
HazardVolume.AgentEntersEvent.Subscribe(OnVolumeChanged)
HazardVolume.AgentExitsEvent.Subscribe(OnVolumeChanged)
# Shared handler for both enter and exit.
OnVolumeChanged(Agent : agent) : void =
# GetAgentsInVolume returns the CURRENT occupants after the event.
AgentsInZone := HazardVolume.GetAgentsInVolume()
if (AgentsInZone.Length > 1):
# Multiple players in the hazard — freeze the wave mid-crash.
WaveCinematic.Pause()
else:
# Back to one (or zero) — resume the cinematic.
WaveCinematic.Play()
Gotchas
1. listenable(?agent) vs listenable(agent) — always check the signature
timer_device.SuccessEvent sends ?agent (optional), while damage_volume_device.AgentEntersEvent sends agent (concrete). If you write OnEscapeSuccess(Agent : agent) for the timer event, it will not compile. Always match the handler parameter type to the event's type parameter exactly.
# WRONG — SuccessEvent is listenable(?agent), not listenable(agent)
OnEscapeSuccess(Agent : agent) : void = ...
# CORRECT — unwrap inside the handler
OnEscapeSuccess(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
ScoreManager.Activate(A)
2. @editable is mandatory — bare device literals do nothing
A bare damage_volume_device{} in your class body creates a new default instance, not the one you placed in the level. Every device you want to control must be declared @editable so UEFN injects the placed instance.
# WRONG — this is a fresh empty instance, not your placed device
HazardVolume : damage_volume_device = damage_volume_device{}
# CORRECT
@editable
HazardVolume : damage_volume_device = damage_volume_device{}
3. SetDamage clamps between 1 and 500
Passing 0 to HazardVolume.SetDamage(0) will silently clamp to 1 — the volume never becomes safe via this call. To neutralise a damage volume, call HazardVolume.Disable() instead.
4. timer_device.Reset vs ResetForAll
Reset(Agent) resets the timer only for that specific agent. ResetForAll() resets it for every agent simultaneously. In a multi-player cove scenario, calling Reset(Agent) on one player's exit won't affect another player's countdown — which is usually what you want, but easy to mix up.
5. cinematic_sequence_device.Play() vs Play(Agent)
The no-argument Play() only works when the device's Activation setting is Everyone. If you've scoped it to a specific team or player, you must pass the instigating agent: WaveCinematic.Play(Agent). Calling the wrong overload silently does nothing in-game.
6. Verse does not auto-convert int to float
SetDamage takes an int. If you compute damage from a float expression (e.g. a lerp), you must explicitly convert: HazardVolume.SetDamage(Int(MyFloat)) — there is no implicit cast.