Overview
The basic_storm_controller_device is a single-phase storm controller — it shrinks the safe zone once, from its starting radius down to the configured end radius, over the configured duration. It lives in the storm_controller_device inheritance chain alongside the multi-phase advanced_storm_controller_device.
When to reach for it:
- You need a storm that closes in once (survival rounds, timed challenges, last-player-standing modes).
- You want to delay the storm's start until a scripted moment (e.g., after a wave of enemies is cleared).
- You need to reset the storm between rounds without restarting the whole session.
- You want to react the instant the storm finishes closing (trigger a cinematic, open a loot vault, end the round).
Key constraint: If you call GenerateStorm() from Verse, you must set the device's Generate Storm On Game Start option to No in the Details panel. Leaving it on Yes means the storm starts automatically and your Verse call may conflict with it.
API Reference
basic_storm_controller_device
A simplified storm device that provides a way to create a single-phase storm and control its basic behaviors. To control multiple phases of the storm see
advanced_storm_controller_device.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse). Inherited members are merged from storm_controller_device.
basic_storm_controller_device<public> := class<concrete><final>(storm_controller_device):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
PhaseEndedEvent |
PhaseEndedEvent<public>:listenable(tuple()) |
Signaled when storm resizing ends. Use this with the On Finish Behavior option for better controls. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
GenerateStorm |
GenerateStorm<public>():void |
Generates the storm. Generate Storm On Game Start must be set to No if you choose to use GenerateStorm. |
DestroyStorm |
DestroyStorm<public>():void |
Destroys the storm. |
storm_controller_device
Base class for various specialized storm devices. See also: *
basic_storm_controller_device*advanced_storm_controller_device
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
storm_controller_device<public> := class<abstract><epic_internal>(creative_device_base):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
PhaseEndedEvent |
PhaseEndedEvent<public>:listenable(tuple()) |
Signaled when storm resizing ends. Use this with the On Finish Behavior option for better controls. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
GenerateStorm |
GenerateStorm<public>():void |
Generates the storm. Generate Storm On Game Start must be set to No if you choose to use GenerateStorm. |
DestroyStorm |
DestroyStorm<public>():void |
Destroys the storm. |
Walkthrough
Scenario: The Vault Opens When the Storm Closes In
A survival island has a locked vault in the center. The storm doesn't start until all enemies are eliminated. The moment the storm finishes closing, the vault door barrier device is disabled, rewarding survivors who made it to the safe zone.
UEFN setup:
- Place a
basic_storm_controller_device. In Details → set Generate Storm On Game Start = No. - Place a
barrier_devicein front of the vault door. - Place a
trigger_devicethat players activate to signal "enemies cleared" (or wire it to a guard spawner'sAllGuardsEliminatedEvent— here we use a trigger for simplicity). - Create a new Verse device, wire all three as
@editablefields.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# vault_storm_manager_device
# - StormController: basic_storm_controller_device (Generate Storm On Game Start = No)
# - VaultBarrier: barrier_device (starts Enabled)
# - EnemiesClearedTrigger: trigger_device (players activate when all enemies are down)
vault_storm_manager_device := class(creative_device):
# Wire this to your basic_storm_controller_device in the Details panel.
@editable
StormController : basic_storm_controller_device = basic_storm_controller_device{}
# Wire this to the barrier_device blocking the vault entrance.
@editable
VaultBarrier : barrier_device = barrier_device{}
# Wire this to a trigger_device players activate to signal enemies are cleared.
@editable
EnemiesClearedTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
# Step 1 — wait for the "enemies cleared" signal before starting the storm.
EnemiesClearedTrigger.TriggeredEvent.Await()
# Step 2 — generate the storm now that enemies are gone.
StormController.GenerateStorm()
# Step 3 — block here until the storm finishes closing.
# PhaseEndedEvent fires a tuple(), so we Await it directly.
StormController.PhaseEndedEvent.Await()
# Step 4 — storm has fully closed in; open the vault!
VaultBarrier.Disable()
Line-by-line breakdown:
| Line | What it does |
|---|---|
@editable StormController |
Exposes the storm device slot in the Details panel so you can wire the placed device. |
@editable VaultBarrier |
Exposes the barrier device slot. |
@editable EnemiesClearedTrigger |
Exposes the trigger slot. |
EnemiesClearedTrigger.TriggeredEvent.Await() |
Suspends until a player fires the trigger — storm stays off until then. |
StormController.GenerateStorm() |
Kicks off the single storm phase. Requires Generate Storm On Game Start = No in Details. |
StormController.PhaseEndedEvent.Await() |
Suspends until the storm finishes shrinking (the phase ends). |
VaultBarrier.Disable() |
Removes the vault barrier, rewarding players who reached the safe zone. |
Note:
PhaseEndedEventis alistenable(tuple()). Calling.Await()on it returns the empty tuple()— you don't need to capture it.
Common patterns
Pattern 1 — Subscribe to PhaseEndedEvent for multi-listener setups
When multiple systems need to react to the storm ending (e.g., spawn loot and play a sound and update a HUD), use Subscribe instead of Await so every handler fires independently.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# storm_phase_subscriber_device
# Demonstrates subscribing a handler to PhaseEndedEvent.
storm_phase_subscriber_device := class(creative_device):
@editable
StormController : basic_storm_controller_device = basic_storm_controller_device{}
@editable
LootSpawner : item_spawner_device = item_spawner_device{}
# Called once at game start — wire up the subscription.
OnBegin<override>()<suspends> : void =
# Subscribe: OnStormPhaseDone is called every time PhaseEndedEvent fires.
StormController.PhaseEndedEvent.Subscribe(OnStormPhaseDone)
# Generate the storm immediately at game start
# (Details panel: Generate Storm On Game Start = No).
StormController.GenerateStorm()
# Handler — PhaseEndedEvent is listenable(tuple()), so the handler takes ().
OnStormPhaseDone() : void =
# Spawn bonus loot at the center safe zone the moment the storm locks in.
LootSpawner.SpawnItem()
Why this matters: Subscribe lets you attach multiple independent handlers to the same event without chaining Await calls in a single coroutine. Each handler runs in its own context when the event fires.
Pattern 2 — DestroyStorm to reset between rounds
In a multi-round survival game you want to clear the storm at the end of each round and regenerate it for the next one.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# storm_round_manager_device
# Runs two storm rounds back-to-back, resetting between them.
storm_round_manager_device := class(creative_device):
@editable
StormController : basic_storm_controller_device = basic_storm_controller_device{}
# A trigger players hit to advance to the next round.
@editable
NextRoundTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
RunRound(1)
RunRound(2)
# Encapsulates a single storm round.
RunRound(RoundNumber : int)<suspends> : void =
# Generate the storm for this round.
StormController.GenerateStorm()
# Wait for the storm phase to fully close.
StormController.PhaseEndedEvent.Await()
# Round over — wait for the host to signal the next round.
NextRoundTrigger.TriggeredEvent.Await()
# Tear down the storm so it can be regenerated cleanly.
StormController.DestroyStorm()
Key point: Always call DestroyStorm() before calling GenerateStorm() again. Calling GenerateStorm() while a storm already exists produces undefined behavior — the device expects a clean state.
Pattern 3 — Delayed storm with a countdown trigger
Give players a grace period before the storm appears, then generate it on demand.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# delayed_storm_device
# Waits 30 seconds after game start, then generates the storm.
delayed_storm_device := class(creative_device):
@editable
StormController : basic_storm_controller_device = basic_storm_controller_device{}
# A countdown timer device set to 30 seconds.
@editable
GracePeriodTimer : timer_device = timer_device{}
OnBegin<override>()<suspends> : void =
# Start the visible countdown so players know the storm is coming.
GracePeriodTimer.Start()
# Block until the timer fires its completion event.
GracePeriodTimer.SuccessEvent.Await()
# Grace period over — unleash the storm.
StormController.GenerateStorm()
# React when the storm finishes closing.
StormController.PhaseEndedEvent.Await()
# Storm fully closed — destroy it to clean up.
StormController.DestroyStorm()
Gotchas
1. Generate Storm On Game Start must be No when calling GenerateStorm()
This is the single most common mistake. If the device's Details panel still has Generate Storm On Game Start = Yes, the storm fires at game start and your GenerateStorm() call may produce a double-storm or be silently ignored. Always flip it to No before writing any Verse that calls GenerateStorm().
2. PhaseEndedEvent is listenable(tuple()) — not listenable(?agent)
Unlike player-triggered events, PhaseEndedEvent carries no payload. When you Subscribe, your handler signature must be HandlerName() : void = (zero parameters). Giving it an Agent parameter will cause a compile error.
# CORRECT
OnPhaseDone() : void =
# do stuff
# WRONG — tuple() carries no agent
OnPhaseDone(Agent : ?agent) : void =
# compile error
3. Call DestroyStorm() before regenerating
The device does not auto-reset. If you call GenerateStorm() while a storm is already active (even a fully-closed one), behavior is undefined. Always pair each GenerateStorm() with a subsequent DestroyStorm() before the next round.
4. @editable is mandatory — you cannot instantiate the device in Verse
basic_storm_controller_device is a <concrete><final> class but it's a placed device — it must exist in the UEFN level and be wired via an @editable field. You cannot write basic_storm_controller_device{} as a meaningful runtime object; the {} default is only a placeholder until the editor wires the real placed instance.
5. Moving the device mid-storm has no effect on the current phase
The inherited TeleportTo / MoveTo methods reposition the device, but existing storms do not retarget — only a newly generated storm will use the new position. If you need a dynamic storm center, call DestroyStorm(), move the device, then call GenerateStorm() again.
6. No int↔float auto-conversion in timer/duration values
If you compute a delay duration in Verse and pass it to any method expecting float, make sure you produce a float literal or cast explicitly. 30 is an int; 30.0 is a float. Mixing them causes a compile error.