Overview
The storm_controller_device is the base class for Fortnite's storm devices — the shrinking circle that keeps players inside a playable area. You never place this abstract class directly; you place one of its two concrete subclasses:
basic_storm_controller_device— a single-phase storm. Great for a quick "closing zone" minigame.advanced_storm_controller_device— a full Battle-Royale-style storm with up to 50 phases, customizable per-phase withadvanced_storm_beacon_devices.
Both share the same small, powerful Verse surface inherited from storm_controller_device: you can GenerateStorm() to start the storm, DestroyStorm() to clear it, and subscribe to PhaseEndedEvent to know exactly when the storm has finished resizing a phase.
Reach for this device whenever you want match pacing driven by code: start the storm when enough players join, end it when a boss is defeated, or chain custom events off each phase shrink (open a vault, spawn loot, announce the next zone).
The one setup rule to remember: if you want to call GenerateStorm() yourself from Verse, set the device option Generate Storm On Game Start to No. Otherwise the storm spawns automatically and your call is redundant (or fights the auto-generated one).
API Reference
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
Let's build a complete round controller: a button starts the match, which generates the storm. Each time a storm phase finishes resizing we announce the next zone to all players. A second button (an "emergency stop") destroys the storm to end the round.
using { /Fortnite.com/Devices }
using { /Fortnite.com }
using { /Verse.org/Simulation }
using { /Verse.org/Chat }
using { /UnrealEngine.com/Temporary/SpatialMath }
# A round controller that drives a Battle Royale storm from Verse.
storm_round_controller := class(creative_device):
# Place an advanced_storm_controller_device in the level and assign it here.
# Remember: set its "Generate Storm On Game Start" option to No.
@editable
Storm : advanced_storm_controller_device = advanced_storm_controller_device{}
# Button players press to start the match.
@editable
StartButton : button_device = button_device{}
# Button to abort the round and clear the storm.
@editable
StopButton : button_device = button_device{}
# Localized text helper — message params need a localized value, not a raw string.
Announce<localizes>(S : string) : message = "{S}"
# Tracks how many phases have finished so we can number the zones.
var PhaseCount : int = 0
OnBegin<override>()<suspends>:void =
# Wire the buttons to our handlers.
StartButton.InteractedWithEvent.Subscribe(OnStartPressed)
StopButton.InteractedWithEvent.Subscribe(OnStopPressed)
# React every time the storm finishes resizing a phase.
Storm.PhaseEndedEvent.Subscribe(OnPhaseEnded)
# InteractedWithEvent hands us the agent who pressed the button.
OnStartPressed(Agent : agent) : void =
Print("Match starting — generating storm!")
Storm.GenerateStorm()
OnStopPressed(Agent : agent) : void =
Print("Round aborted — destroying storm.")
Storm.DestroyStorm()
set PhaseCount = 0
# PhaseEndedEvent is listenable(tuple()) — its handler takes no useful payload.
OnPhaseEnded() : void =
set PhaseCount += 1
Print("Storm phase {PhaseCount} finished resizing.")
Line by line:
- The
@editable Storm : advanced_storm_controller_devicefield is what lets Verse talk to the placed device. Without an editable field you cannot callStorm.GenerateStorm()— a bare device call fails with Unknown identifier. Announce<localizes>is our localized-text helper. Any device API that wants amessage(like HUD text) needs this — there is noStringToMessage.- In
OnBeginwe subscribe all three handlers. Event handlers are methods at class scope; we subscribe them insideOnBegin. OnStartPressedcallsStorm.GenerateStorm()— this is the real device method that spawns the storm. Because we set Generate Storm On Game Start to No, this is what actually kicks off the circle.OnStopPressedcallsStorm.DestroyStorm()to wipe the storm and resets our counter.OnPhaseEndedfires every time a phase finishes shrinking. BecausePhaseEndedEventislistenable(tuple()), the handler takes no parameters — we just bump and report a counter.
Common patterns
Pattern 1 — Auto-start the storm after a delay (GenerateStorm)
No button needed: wait a few seconds at match start, then generate the storm so latecomers can spawn in first.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
storm_auto_start := class(creative_device):
@editable
Storm : basic_storm_controller_device = basic_storm_controller_device{}
OnBegin<override>()<suspends>:void =
Print("Grace period — players spawning in...")
Sleep(15.0)
Print("Grace period over — storm incoming!")
Storm.GenerateStorm()
Pattern 2 — Chain logic off each finished phase (PhaseEndedEvent)
When a storm phase finishes resizing, enable a VFX spawner to telegraph the next danger zone.
using { /Fortnite.com/Devices }
using { /Fortnite.com }
using { /Verse.org/Simulation }
storm_phase_fx := class(creative_device):
@editable
Storm : advanced_storm_controller_device = advanced_storm_controller_device{}
@editable
ZoneFX : vfx_spawner_device = vfx_spawner_device{}
OnBegin<override>()<suspends>:void =
Storm.PhaseEndedEvent.Subscribe(OnPhaseEnded)
Storm.GenerateStorm()
OnPhaseEnded() : void =
Print("Phase complete — flashing the next zone marker.")
ZoneFX.Restart()
Pattern 3 — End the round by destroying the storm (DestroyStorm)
When a boss is defeated (its trigger_device fires), clear the storm to signal the round is over.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
storm_boss_clear := class(creative_device):
@editable
Storm : advanced_storm_controller_device = advanced_storm_controller_device{}
@editable
BossDefeatedTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends>:void =
Storm.GenerateStorm()
BossDefeatedTrigger.TriggeredEvent.Subscribe(OnBossDefeated)
# TriggeredEvent hands us an ?agent — unwrap it if you need who triggered it.
OnBossDefeated(Agent : ?agent) : void =
if (Player := Agent?):
Print("Boss defeated by a player — clearing the storm.")
Storm.DestroyStorm()
Gotchas
GenerateStorm()needs the right option. If Generate Storm On Game Start is left at Yes, the storm spawns on its own and yourGenerateStorm()call may do nothing or conflict. Set it to No when driving the storm from Verse.PhaseEndedEventcarries no payload. It'slistenable(tuple()), so its handler takes zero parameters —OnPhaseEnded() : void. Don't try to declare anagentparameter; that won't match the subscription.- Subscribe inside
OnBegin. Handlers are class methods, but the.Subscribe(...)calls belong inOnBegin<override>()<suspends>:void. Subscribing at field-init time won't work. - You must use an editable field, and the right subclass.
storm_controller_deviceis<abstract>— you cannot place it. Declare your field asbasic_storm_controller_deviceoradvanced_storm_controller_deviceto match the device you actually placed in the level. DestroyStorm()afterGenerateStorm()only. CallingDestroyStorm()when no storm exists is harmless but does nothing — track your own state if your logic depends on whether a storm is active.- Other devices'
?agentevents still need unwrapping. When you wire a button or trigger to start/stop the storm, those events hand you anagentor?agent. Unwrap an optional withif (P := Agent?):before using it.