Reference Devices compiles

Multiple Return Values in Verse: Tuples and Tracker APIs

Sometimes one question needs several answers at once: what's this player's score, is the tracker running for them, AND did they hit the target? Verse has no `return a, b, c` — instead it combines tuples with `<decides>` success/failure to express every 'multiple return values' scenario. In this guide you'll fold real Tracker device calls into a tuple-returning helper and read all three answers from a single trigger event.

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

What you'll learn

  • How Verse expresses "multiple return values" with tuples like tuple(logic, logic) and destructuring via .0 / .1.
  • How <decides> calls such as Tracker.IsActive[Agent] and Tracker.HasReachedTarget[Agent] "return" a success/failure that you read in an if — they do NOT return logic, so you never call them bare.
  • How overloaded device methods (GetValue() vs GetValue(Agent)) let you pick which value to "return".
  • How to wire a trigger event to read all of these at once.

How it works

Verse gives you two complementary tools:

  1. Tuples pack several values into one. A function typed tuple(logic, logic) returns two values; you build one by stating (A, B) as the final expression and read it back with Result(0) / Result(1).

  2. <decides> functions are fallible. CoinTracker.IsActive[Agent] uses [] brackets because it may fail. It never hands back a logic — it either succeeds (the then: branch runs) or fails (the else: runs). To turn that success/failure INTO a logic you can pass around, wrap it in an if and set a flag.

⚠️ Common mistake: writing CoinTracker.IsActive[A] as a bare statement. [] fallible calls may ONLY appear in a failure context — an if/for condition, a <decides> body, or a not/or operand.

  1. Overloads: GetValue() returns the whole-match value, GetValue(Agent:agent) returns that player's value, and GetValue(TeamIndex:int) returns the team's value. These use () parens because they simply return an int — they are not fallible. Choosing the overload is how you "return" the value you actually want.

We combine IsActive and HasReachedTarget into ONE helper that returns both as a tuple(logic, logic), so the caller gets two answers from one call, then reads the numeric value with a GetValue overload.

Let's build it

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

# Finish-line challenge: read three answers from one trigger check-in.
finish_line_challenge := class<concrete>(creative_device):

    # The plate a player steps on to "check in" at the finish line.
    @editable
    CheckPlate : trigger_device = trigger_device{}

    # The Tracker counting each player's coins.
    @editable
    CoinTracker : tracker_device = tracker_device{}

    # Returns TWO answers at once as a tuple:
    #   result(0) = is the tracker active for this agent?
    #   result(1) = has this agent reached the target?
    # Both come from <decides> calls folded into plain logic values.
    ReadStatus(A : agent) : tuple(logic, logic) =
        var WasActive : logic = false
        # IsActive[A] is fallible — read its success inside an if.
        if (CoinTracker.IsActive[A]):
            set WasActive = true
        var WasDone : logic = false
        if (CoinTracker.HasReachedTarget[A]):
            set WasDone = true
        # State the tuple as the final expression to "return" both.
        (WasActive, WasDone)

    OnBegin<override>()<suspends> : void =
        # Device events subscribe WITHOUT () after the event name.
        CheckPlate.TriggeredEvent.Subscribe(OnCheckIn)

    OnCheckIn(Agent : ?agent) : void =
        # TriggeredEvent hands us ?agent — unwrap it before use.
        if (A := Agent?):
            # One call, two answers via tuple destructuring.
            Result := ReadStatus(A)
            WasActive := Result(0)
            WasDone := Result(1)
            # Overloaded GetValue(Agent) returns THIS player's value (int).
            PlayerValue := CoinTracker.GetValue(A)
            # GetValue() with no args returns the whole-match value.
            MatchValue := CoinTracker.GetValue()
            if (WasDone?):
                Print("Finished! Player coins = {PlayerValue}, match total = {MatchValue}")
            else if (WasActive?):
                Print("Still tracking. Player coins = {PlayerValue}")
            else:
                Print("Tracker not active for this player yet.")

Try it yourself

  1. Place a Tracker device and a Trigger device in your level.
  2. Add this Verse device, then assign CoinTracker and CheckPlate in its Details panel.
  3. Configure the Tracker to count a stat and set a Target Value.
  4. Step on the trigger before and after reaching the target — watch the log print the correct branch each time.
  5. Extend it: add a GetValue(TeamIndex:int) read for the player's team total, or use SetValue(A, 0) to reset a player's counter after they finish.

Recap

  • Verse has no return a, b, c; use a tuple return (tuple(logic, logic)) and read fields with Result(0) / Result(1).
  • <decides> calls like IsActive[A] and HasReachedTarget[A] use [] and may ONLY appear in a failure context — read their success in an if, never call them bare.
  • Overloaded GetValue() / GetValue(Agent) / GetValue(TeamIndex) use () and return an int directly — pick the overload to choose which value you "return".
  • Device events (TriggeredEvent) subscribe without (); unwrap ?agent with if (A := Agent?): before using it.

Build your own lesson with multiple_return_values

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 →