Reference Devices

skilled_interaction_device: Build Timing Mini-Games in Verse

Ever wanted a lockpicking mini-game where the player has to hit the sweet spot on a moving meter? The skilled_interaction_device gives you exactly that — a scrubber that sweeps from 0.0 to 1.0 with Good and Perfect zones — and Verse lets you react to every input and run real game logic when they nail it.

Updated
The code on this reference page is provided as-is and did not pass the latest compile check — treat the examples as a starting point and verify in your project.
Watch the Knotskilled_interaction_device in ~90 seconds.

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 @editable fields let you drop the real placed devices into the slots in UEFN. Without a class(creative_device) field, you can't call any device method.
  • MakeText<localizes> is how you turn a raw string into a message for ShowMessage — there is no StringToMessage.
  • In OnBegin we wire the plate's TriggeredEvent, then subscribe handlers to the three input-quality events and four lifecycle events.
  • OnPlateStepped receives ?agent; we unwrap with if (A := Agent?). BeginInteraction is <decides>, so we call it inside if (...) to handle the failure path.
  • The input events deliver tuple(agent, float); Data(1) is the scrubber position from 0.0 to 1.0.
  • On success we Enable() and Restart() 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

  • BeginInteraction and EndInteraction are <decides>. They can fail, so you must call them inside a failure context like if (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, and BadInputTriggeredEvent hand you tuple(agent, float). Index with Data(0) for the agent and Data(1) for the scrubber position.
  • Lifecycle events hand a plain agent (not ?agent), but trigger_device.TriggeredEvent hands a ?agent — remember to unwrap the trigger's with if (A := Agent?).
  • EmptyQueueEvent is listenable(tuple()). Its handler takes a tuple() parameter, not an agent.
  • RemoveFromQueue does not end interactions. It only touches the queue; a player already interacting stays interacting. Use EndInteraction for active agents.
  • message needs localization. Pass MakeText("...") to ShowMessage, never a raw string — there is no StringToMessage helper.
  • GetPlayerUI is <decides> and needs player[A] — wrap in if (UI := GetPlayerUI[player[A]]).

Guides & scripts that use skilled_interaction_device

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

Build your own lesson with skilled_interaction_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 →