Reference Devices compiles

storm_controller_device: Closing the Circle on Cue

Every Battle Royale needs a shrinking storm that herds players toward the final showdown. The storm_controller_device is how you spawn, destroy, and react to that storm from Verse — so you can trigger it when a match actually starts and run logic the moment a phase finishes resizing.

Updated Examples verified on the live UEFN compiler
Watch the Knotstorm_controller_device in ~90 seconds.

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 with advanced_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_device field is what lets Verse talk to the placed device. Without an editable field you cannot call Storm.GenerateStorm() — a bare device call fails with Unknown identifier.
  • Announce<localizes> is our localized-text helper. Any device API that wants a message (like HUD text) needs this — there is no StringToMessage.
  • In OnBegin we subscribe all three handlers. Event handlers are methods at class scope; we subscribe them inside OnBegin.
  • OnStartPressed calls Storm.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.
  • OnStopPressed calls Storm.DestroyStorm() to wipe the storm and resets our counter.
  • OnPhaseEnded fires every time a phase finishes shrinking. Because PhaseEndedEvent is listenable(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 your GenerateStorm() call may do nothing or conflict. Set it to No when driving the storm from Verse.
  • PhaseEndedEvent carries no payload. It's listenable(tuple()), so its handler takes zero parametersOnPhaseEnded() : void. Don't try to declare an agent parameter; that won't match the subscription.
  • Subscribe inside OnBegin. Handlers are class methods, but the .Subscribe(...) calls belong in OnBegin<override>()<suspends>:void. Subscribing at field-init time won't work.
  • You must use an editable field, and the right subclass. storm_controller_device is <abstract> — you cannot place it. Declare your field as basic_storm_controller_device or advanced_storm_controller_device to match the device you actually placed in the level.
  • DestroyStorm() after GenerateStorm() only. Calling DestroyStorm() 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' ?agent events still need unwrapping. When you wire a button or trigger to start/stop the storm, those events hand you an agent or ?agent. Unwrap an optional with if (P := Agent?): before using it.

Device Settings & Options

The storm_controller_device User Options panel in the UEFN editor — every setting you can tune.

Storm Controller Device settings and options panel in the UEFN editor (view 1 of 3)
Storm Controller Device — User Options (1 of 3) in the UEFN editor
Storm Controller Device settings and options panel in the UEFN editor (view 2 of 3)
Storm Controller Device — User Options (2 of 3) in the UEFN editor
Storm Controller Device settings and options panel in the UEFN editor (view 3 of 3)
Storm Controller Device — User Options (3 of 3) in the UEFN editor

Guides & scripts that use storm_controller_device

Step-by-step tutorials that put this object to work.

Build your own lesson with storm_controller_device

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →