Overview
The skilled_interaction_device powers skill-based timing minigames: a scrubber sweeps from 0.0 to 1.0, and the player must provide input while it's inside a Good Zone or the tighter Perfect Zone. Think lockpicking, fishing, safe-cracking, or a QTE to disarm a bomb.
Reach for it when a plain button or trigger isn't enough — when you want the quality of the player's timing to matter. The device fires distinct events for Perfect, Good, and Bad inputs (each carrying the scrubber position), plus lifecycle events for when an interaction starts, succeeds, fails, or is canceled. It also has a built-in queue so multiple agents can line up for a single station, with events and methods to inspect and manage that queue.
From Verse you don't read input directly — you call BeginInteraction to put an agent on the device, then subscribe to its events to grant rewards, play VFX, or punish failure.
API Reference
skilled_interaction_device
Used to create a minigame which expects specific input timing from the player. Visuals may vary, but always feature a scrubber that moves from 0.0 to 1.0. Good and perfect zones are defined on the device, describing at what point the player must provide input to succeed.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
skilled_interaction_device<public> := class<concrete><final>(creative_device_base, enableable):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
PerfectInputTriggeredEvent |
PerfectInputTriggeredEvent<public>:listenable(tuple(agent, float)) |
Signaled when the agent provides a perfect input. Perfect input occurs when input is provided while the scrubber is within the device's Perfect Zone. Sends the agent that provided the input. Sends the float position of the meter scr |
GoodInputTriggeredEvent |
GoodInputTriggeredEvent<public>:listenable(tuple(agent, float)) |
Signaled when the agent provides a good input. Good input occurs when input is provided while the scrubber is within the Good Zone (excluding the Perfect Zone). Sends the agent that provided the input. Sends the float position of |
BadInputTriggeredEvent |
BadInputTriggeredEvent<public>:listenable(tuple(agent, float)) |
Signaled when the agent provides a bad input. Bad input occurs when input is provided while the scrubber is outside the Good Zone. Sends the agent that provided the input. Sends the float position of the meter scrubber from 0.0 to |
InteractionStartedEvent |
InteractionStartedEvent<public>:listenable(agent) |
Signaled when the agent starts an interaction. Input is the agent which started interacting with the device. |
InteractionSucceededEvent |
InteractionSucceededEvent<public>:listenable(agent) |
Signaled when the agent succeeds at the interaction. The interaction is completed depending on the device's configuration. * Once the device's Success Target is reached. * Upon a perfect input if using Instant Success Perfect Behavior |
InteractionFailedEvent |
InteractionFailedEvent<public>:listenable(agent) |
Signaled when the agent fails the interaction. The interaction can be failed in several ways * If the number of bad inputs reaches the Failure Limit. * If the interaction has an Interaction Time Limit that expires. |
InteractionCanceledEvent |
InteractionCanceledEvent<public>:listenable(agent) |
Signaled when the interaction is interrupted. The interaction can be interrupted in several ways. * If the participating agent is eliminated or enters a downed state. * If Deactivate is called while the interaction is in progress.* If * |
QueueAgentEvent |
QueueAgentEvent<public>:listenable(agent) |
Signaled when the agent is put into the queue but has not started an interaction. If an agent skips the queue and is put directly into interacting, this event is skipped. |
RemoveAgentFromQueueEvent |
RemoveAgentFromQueueEvent<public>:listenable(agent) |
Signaled when the agent has been removed from the queue. This can happen in a few ways * An agent is removed without advancing into an interaction. * If Disable is called while an agent is in the queue. * The queue size is shrunk, causi |
AdvanceAgentFromQueueEvent |
AdvanceAgentFromQueueEvent<public>:listenable(agent) |
Signaled when the agent moves from the queue into interacting. An agent enters the queue if BeginInteract is called for them but there is no room for them to begin an interaction given the current configuration. This event will fire when |
EmptyQueueEvent |
EmptyQueueEvent<public>:listenable(tuple()) |
Fired when the queue empties of all queued agents. The queue has to have at least one agent in it for this to fire. If BeginInteraction is called and a queue is not created, then this event will not be triggered. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
BeginInteraction |
BeginInteraction<public>(Agent:agent)<transacts><decides>:void |
Begins the interaction for the provided agent. Will cancel any other interactions already in progress for the agent if a queue is not being utilized. If a queue is being utilized and duplicate player entries are not allowed then any dup |
BeginInteraction |
BeginInteraction<public>(Agents:[]agent)<transacts><decides>:void |
Begins the interaction for the provided agent(s). Will cancel any other interactions already in progress for the agent if a queue is not being utilized. If a queue is being utilized and duplicate player entries are not allowed then any |
EndInteraction |
EndInteraction<public>(Agent:agent)<transacts><decides>:void |
Cancels the interaction, if in progress, for the provided agent. Does not clear the queue of any agent awaiting interaction |
EndInteraction |
EndInteraction<public>(Agents:[]agent)<transacts><decides>:void |
Cancels the interaction, if in progress, for the provided agent(s). Does not clear the queue of any agent awaiting interaction |
RemoveFromQueue |
RemoveFromQueue<public>(Agents:[]agent, AllEntries:logic):int |
Remove agent(s) from the queue, has the option to remove all entries that exist for that agent or not This does not end the interaction for any agents. Returns the number of agents that were removed. |
ClearQueue |
ClearQueue<public>():[]agent |
Clears the queue of all agent(s). Returns an array of all the players that were still in the queue. |
GetQueue |
GetQueue<public>():[]agent |
Gets the queue of agent(s) who are awaiting interaction. |
GetInteractingAgents |
GetInteractingAgents<public>():[]agent |
Gets the agent(s) who are currently interacting |
Walkthrough
Let's build a lockpick vault. When a player steps on a pressure plate (a trigger_device), we start a skilled interaction. A perfect input upgrades a reward prop's mesh and instantly succeeds; a good input nudges progress; bad inputs are tracked. When the interaction succeeds we grant access by enabling a VFX burst on the now-open vault; if it fails or is canceled we reset.
using { /Fortnite.com/Devices }
using { /Fortnite.com }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/Diagnostics }
lockpick_vault := class(creative_device):
# The skilled interaction device that drives the lockpick minigame.
@editable
Lockpick : skilled_interaction_device = skilled_interaction_device{}
# Pressure plate the player steps on to start picking the lock.
@editable
StartPlate : trigger_device = trigger_device{}
# VFX burst we fire when the vault opens.
@editable
OpenVFX : vfx_spawner_device = vfx_spawner_device{}
# Helper to build localized messages for the player UI.
MakeText<localizes>(S : string) : message = "{S}"
OnBegin<override>()<suspends>:void =
# Step on the plate to begin the lockpick interaction.
StartPlate.TriggeredEvent.Subscribe(OnPlateStepped)
# React to the player's timing inputs.
Lockpick.PerfectInputTriggeredEvent.Subscribe(OnPerfect)
Lockpick.GoodInputTriggeredEvent.Subscribe(OnGood)
Lockpick.BadInputTriggeredEvent.Subscribe(OnBad)
# React to the interaction lifecycle.
Lockpick.InteractionStartedEvent.Subscribe(OnStarted)
Lockpick.InteractionSucceededEvent.Subscribe(OnSucceeded)
Lockpick.InteractionFailedEvent.Subscribe(OnFailed)
Lockpick.InteractionCanceledEvent.Subscribe(OnCanceled)
# trigger_device hands us a ?agent — unwrap before use.
OnPlateStepped(Agent : ?agent) : void =
if (A := Agent?):
# BeginInteraction <decides> can fail, so guard it.
if (Lockpick.BeginInteraction[A]):
Print("Lockpick started for player")
OnStarted(A : agent) : void =
Print("Interaction started")
# Perfect/Good/Bad hand us a tuple(agent, float) scrubber position.
OnPerfect(Data : tuple(agent, float)) : void =
Pos := Data(1)
Print("PERFECT input at scrubber {Pos}")
OnGood(Data : tuple(agent, float)) : void =
Pos := Data(1)
Print("Good input at scrubber {Pos}")
OnBad(Data : tuple(agent, float)) : void =
Pos := Data(1)
Print("Bad input at scrubber {Pos}")
OnSucceeded(A : agent) : void =
# Vault cracked! Fire the open VFX and tell the player.
OpenVFX.Enable()
OpenVFX.Restart()
if (UI := GetPlayerUI[player[A]]):
UI.ShowMessage(MakeText("Vault unlocked!"))
OnFailed(A : agent) : void =
if (UI := GetPlayerUI[player[A]]):
UI.ShowMessage(MakeText("The lock jammed — try again."))
OnCanceled(A : agent) : void =
Print("Lockpick interaction canceled")
Line by line:
- The
@editablefields let you drop the real placed devices into the slots in UEFN. Without aclass(creative_device)field, you can't call any device method. MakeText<localizes>is how you turn a raw string into amessageforShowMessage— there is noStringToMessage.- In
OnBeginwe wire the plate'sTriggeredEvent, then subscribe handlers to the three input-quality events and four lifecycle events. OnPlateSteppedreceives?agent; we unwrap withif (A := Agent?).BeginInteractionis<decides>, so we call it insideif (...)to handle the failure path.- The input events deliver
tuple(agent, float);Data(1)is the scrubber position from0.0to1.0. - On success we
Enable()andRestart()the VFX spawner to play a burst, and show a UI message.
Common patterns
Queue several players at one station
A shared lockpicking bench: as players arrive we add them. The device queues anyone who doesn't fit, and we watch the queue events to know who's waiting versus active.
using { /Fortnite.com/Devices }
using { /Fortnite.com }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
lockpick_queue := class(creative_device):
@editable
Lockpick : skilled_interaction_device = skilled_interaction_device{}
@editable
JoinPlate : trigger_device = trigger_device{}
OnBegin<override>()<suspends>:void =
JoinPlate.TriggeredEvent.Subscribe(OnJoin)
Lockpick.QueueAgentEvent.Subscribe(OnQueued)
Lockpick.AdvanceAgentFromQueueEvent.Subscribe(OnAdvanced)
Lockpick.EmptyQueueEvent.Subscribe(OnEmpty)
OnJoin(Agent : ?agent) : void =
if (A := Agent?):
if (Lockpick.BeginInteraction[A]):
# Either started or queued depending on capacity.
Print("Player added to lockpick station")
OnQueued(A : agent) : void =
# Report how many are now waiting.
Waiting := Lockpick.GetQueue()
Print("Player queued. {Waiting.Length} waiting in line")
OnAdvanced(A : agent) : void =
Active := Lockpick.GetInteractingAgents()
Print("Player advanced to picking. {Active.Length} now interacting")
OnEmpty(Empty : tuple()) : void =
Print("Queue is now empty")
Clear or trim the queue when a round ends
When the heist timer runs out you may want to kick everyone off the bench and out of line.
using { /Fortnite.com/Devices }
using { /Fortnite.com }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
lockpick_reset := class(creative_device):
@editable
Lockpick : skilled_interaction_device = skilled_interaction_device{}
@editable
EndRoundButton : button_device = button_device{}
OnBegin<override>()<suspends>:void =
EndRoundButton.InteractedWithEvent.Subscribe(OnEndRound)
Lockpick.RemoveAgentFromQueueEvent.Subscribe(OnRemoved)
OnEndRound(A : agent) : void =
# End any in-progress interactions for currently active agents.
Active := Lockpick.GetInteractingAgents()
if (Lockpick.EndInteraction[Active]):
Print("Ended {Active.Length} active interactions")
# Clear everyone out of the queue; returns who was waiting.
Cleared := Lockpick.ClearQueue()
Print("Cleared {Cleared.Length} queued players")
OnRemoved(A : agent) : void =
Print("A player was removed from the queue")
Remove one specific agent from the queue
If a single player disconnects or leaves the area, drop just their entries without touching anyone else.
using { /Fortnite.com/Devices }
using { /Fortnite.com }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
lockpick_leave := class(creative_device):
@editable
Lockpick : skilled_interaction_device = skilled_interaction_device{}
@editable
LeavePlate : trigger_device = trigger_device{}
OnBegin<override>()<suspends>:void =
LeavePlate.TriggeredEvent.Subscribe(OnLeave)
OnLeave(Agent : ?agent) : void =
if (A := Agent?):
# RemoveFromQueue takes an array and a logic (remove all entries?).
RemovedCount := Lockpick.RemoveFromQueue(array{A}, true)
Print("Removed {RemovedCount} queue entries for the leaving player")
Gotchas
BeginInteractionandEndInteractionare<decides>. They can fail, so you must call them inside a failure context likeif (Lockpick.BeginInteraction[A]):. Calling them as plain statements won't compile.- The array overloads use
[], not().BeginInteraction[A]for a single agent,BeginInteraction[Active]for an[]agent— both are invocations of a<decides>function, so use square brackets. - Input events carry a tuple, not just an agent.
PerfectInputTriggeredEvent,GoodInputTriggeredEvent, andBadInputTriggeredEventhand youtuple(agent, float). Index withData(0)for the agent andData(1)for the scrubber position. - Lifecycle events hand a plain
agent(not?agent), buttrigger_device.TriggeredEventhands a?agent— remember to unwrap the trigger's withif (A := Agent?). EmptyQueueEventislistenable(tuple()). Its handler takes atuple()parameter, not an agent.RemoveFromQueuedoes not end interactions. It only touches the queue; a player already interacting stays interacting. UseEndInteractionfor active agents.messageneeds localization. PassMakeText("...")toShowMessage, never a raw string — there is noStringToMessagehelper.GetPlayerUIis<decides>and needsplayer[A]— wrap inif (UI := GetPlayerUI[player[A]]).