Overview
A tuple is Verse's way of bundling two or more values of possibly-different types into one unit. You access its parts by position — MyTuple(0), MyTuple(1) — or you destructure it into named locals in one line. This matters because a huge number of UEFN device events don't hand your subscriber a plain agent; they hand you a tuple payload.
The Class Selector's ClassChangedEvent is the perfect teaching case: it is a listenable(tuple(agent, int)). When a player switches class, your handler receives BOTH who switched (the agent) and which class index (the int) — packed as tuple(agent, int). If you only read the agent, you throw away the class number; if you only read the int, you don't know who to reward. Naming both elements is how you build real game logic: "when Player X picks class 2 (the Sniper), spawn a sniper turret pointed at the arena and boost its damage."
Reach for tuple destructuring whenever an event's type is written listenable(tuple(...)) — which includes ClassChangedEvent, the VFX spawner's EffectEnabledEvent/EffectDisabledEvent (tuple() — an empty tuple), and countless others across the API surface. The device methods themselves (SetDamage, SetActivationRange, Hide, Enable) are what you CALL in response.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: a class-based arena. Players use a Class Selector UI. When someone picks the "Sniper" class (index 2), we crank a defense turret's damage and range way up and clear its current target so it re-acquires fresh. When they pick any other class, we drop the turret back to a gentle setting. We read both tuple elements — the agent (to log/gate who) and the int (to branch on the class).
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }
# A class-based arena: switching to the Sniper class supercharges the turret.
class_arena_device := class(creative_device):
# The Class Selector UI device placed in the level.
@editable
ClassSelector : class_selector_ui_device = class_selector_ui_device{}
# The Automated Turret we will reconfigure.
@editable
Turret : automated_turret_device = automated_turret_device{}
# Which class index counts as the "Sniper".
@editable
SniperClassIndex : int = 2
OnBegin<override>()<suspends>:void =
# ClassChangedEvent is listenable(tuple(agent, int)).
# Subscribe hands our method the whole tuple.
ClassSelector.ClassChangedEvent.Subscribe(OnClassChanged)
# The handler receives the FULL tuple payload as one parameter.
OnClassChanged(Payload : tuple(agent, int)) : void =
# Destructure the tuple into two named locals in one line.
Player := Payload(0) # the agent who switched
ClassIndex := Payload(1) # the int class index
if (ClassIndex = SniperClassIndex):
# Sniper picked: heavy hitter, long range.
Turret.SetDamage(75.0)
Turret.SetActivationRange(90.0)
Turret.SetTargetRange(90.0)
# Clear so it re-acquires the best target immediately.
Turret.ClearTarget()
else:
# Any other class: gentle turret.
Turret.SetDamage(15.0)
Turret.SetActivationRange(30.0)
Turret.SetTargetRange(30.0)
Line by line:
- The
@editablefields let you drag your real placed devices onto this script in UEFN. Without them,Turret.SetDamage(...)would fail with Unknown identifier. ClassSelector.ClassChangedEvent.Subscribe(OnClassChanged)wires the event to our method. The event's type islistenable(tuple(agent, int)), so the handler must accept exactly one parameter of typetuple(agent, int).Payload(0)andPayload(1)read the tuple by index — position0is theagent, position1is theint. Giving them names (Player,ClassIndex) makes the branching readable.if (ClassIndex = SniperClassIndex):branches on the int element. On a match we call the turret's real reconfiguration methods and finish withClearTarget()so it re-picks the best target under the new settings.- Note the floats:
SetDamage,SetActivationRange, andSetTargetRangeall takefloat, so we pass75.0, not75— Verse does not auto-convert int to float.
Common patterns
Destructure directly in the parameter with element bindings
You can also pull the tuple apart at read time and immediately gate on the class. Here we only care about the agent element to display the turret again (Enable isn't on the turret, so we use ClearTarget as the game action tied to the agent's choice).
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
class reset_on_switch_device := class(creative_device):
@editable
ClassSelector : class_selector_ui_device = class_selector_ui_device{}
@editable
Turret : automated_turret_device = automated_turret_device{}
OnBegin<override>()<suspends>:void =
ClassSelector.ClassChangedEvent.Subscribe(OnSwitch)
OnSwitch(Payload : tuple(agent, int)) : void =
# Read just the int element to log which class, and always
# clear the turret so a fresh fight starts on every switch.
NewClass := Payload(1)
# GetTarget returns ?agent; unwrap before acting on it.
if (Current := Turret.GetTarget?):
Turret.ClearTarget()
# Retune based on the class number.
if (NewClass = 0):
Turret.SetTargetRange(20.0)
else:
Turret.SetTargetRange(60.0)
Reacting to an EMPTY tuple event
Some events carry tuple() — an empty tuple with no elements. The VFX spawner's EffectEnabledEvent is one: it just means "it happened." Your handler takes tuple() and reads nothing from it.
using { /Fortnite.com }
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
class vfx_reaction_device := class(creative_device):
@editable
Fireworks : vfx_spawner_device = vfx_spawner_device{}
@editable
Smoke : vfx_spawner_device = vfx_spawner_device{}
OnBegin<override>()<suspends>:void =
# EffectEnabledEvent is listenable(tuple()) — an empty payload.
Fireworks.EffectEnabledEvent.Subscribe(OnFireworksOn)
# Kick off the fireworks so the event can fire.
Fireworks.Enable()
Fireworks.Restart()
# Empty tuple = no data, just a signal.
OnFireworksOn(Payload : tuple()) : void =
# When fireworks light up, disable the smoke effect.
Smoke.Disable()
Passing tuple elements straight into a method
Because you can access elements by index, you can feed one element to a method and another to a branch in the same handler — a common shape when the tuple mixes "who" and "how much."
using { /Fortnite.com/Devices }
using { /Fortnite.com/Devices/Patchwork }
using { /Verse.org/Simulation }
class damage_scaler_device := class(creative_device):
@editable
ClassSelector : class_selector_ui_device = class_selector_ui_device{}
@editable
Turret : automated_turret_device = automated_turret_device{}
OnBegin<override>()<suspends>:void =
ClassSelector.ClassChangedEvent.Subscribe(OnPicked)
OnPicked(Payload : tuple(agent, int)) : void =
# Turn the class index into a damage value: higher class = higher damage.
ClassIndex := Payload(1)
# int -> float explicitly, then scale.
DamageValue := 20.0 + (ClassIndex * 1.0) * 10.0
Turret.SetDamage(DamageValue)
Gotchas
- The handler parameter type must match the event exactly. For
listenable(tuple(agent, int))the method takes ONE parameter of typetuple(agent, int)— not two separateagent/intparams. Verse tuple expansion happens when you pass a tuple into a multi-arg function, but a subscribed event handler receives the tuple whole. - Empty tuples still need a parameter.
EffectEnabledEventislistenable(tuple()); your handler must declare(Payload : tuple())even though there's nothing to read. - Index access is by position, and the types differ.
Payload(0)is theagent,Payload(1)is theint. Mixing them up is a compile error because the types don't match where you use them. GetTarget()returns?agent, an optional. You must unwrap it (if (T := Turret.GetTarget?):) before treating it as an agent — it can legitimately be empty when the turret sees no one.- No int↔float auto-conversion.
SetDamage,SetActivationRange, andSetTargetRangetakefloat. Pass75.0, and when deriving from anintclass index multiply by1.0to lift it into float first. - Range values are clamped.
SetActivationRange/SetTargetRangeclamp between2.0and100.0meters, and setting the target range below2.0disables targeting — so tiny values silently turn the turret off rather than erroring. message-typed params want localized text, not raw strings. If a device method takes amessage(like some naming methods elsewhere), declareMyText<localizes>(S:string):message = "{S}"and passMyText("...")— there is noStringToMessage.