Reference Verse compiles

Return an Option: Say 'Maybe' Safely in Verse

Sometimes the honest answer is 'maybe' — the buoy MIGHT be one of ours, the player MIGHT have picked a valid slot. Verse models this with the option type (?type), and a function that returns one lets you handle both cases without crashing. We'll wire it to a real button, show the result on the player's HUD, and fire a cannon trigger only when there's a real value.

Updated Examples verified on the live UEFN compiler

Overview

An option in Verse is a value that may or may not be present. Its type is written with a leading ? — so ?int is "maybe an int" and ?agent is "maybe an agent". There are exactly two things an option can be:

  • option{X} — a value is present (holds X)
  • false — nothing is there (empty)

The game problem this solves: searching for something that might not exist. Which player is standing on the dock plate? Is there still a beach ball in the lost-and-found box? What's the index of the treasure in this array? All of these can fail to find anything, and an option is how a function honestly reports "I found it: here it is" versus "nothing here."

You'll see options everywhere in UEFN: a listenable(?agent) event hands your handler an ?agent you must unwrap, Array.Find gives back a ?int index, and your own helper functions can return options too. This article is about the pattern of returning an option from a function you write, then safely reading it back.

Reach for return option{...} / return false whenever a lookup can legitimately come up empty — it's cleaner and safer than sentinel values like -1.

API Reference

(API surface could not be resolved for this device.)

Walkthrough

The sunny scene: A beach cove has a row of lifeguard lockers. Each locker is numbered. When a player steps on the dock trigger, we search our list of stored item names for the one matching that locker number. If we find it, we announce it; if the locker is empty, we say so. The search function returns an option — that's the star of the show.

lifeguard_lockers := class(creative_device):

    # The dock plate the player steps on to check a locker
    @editable DockPlate : trigger_device = trigger_device{}

    # A HUD device to show the result message to the player
    @editable ResultSign : hud_message_device = hud_message_device{}

    # Our "stored items" — index in this array = locker number.
    # Empty string means the locker is empty.
    var LockerContents : []string = array{"Beach Ball", "", "Surfboard", "", "Sunscreen"}

    # Which locker the plate currently checks (bump it each step for the demo)
    var CurrentLocker : int = 0

    # localizes lets us pass a string into a message-typed parameter
    Msg<localizes>(S : string) : message = "{S}"

    OnBegin<override>()<suspends>:void =
        DockPlate.TriggeredEvent.Subscribe(OnDockStep)

    # A listenable(?agent) event hands us an ?agent — we unwrap it.
    OnDockStep(Agent : ?agent) : void =
        if (Player := Agent?):
            # Call our option-returning function
            if (ItemName := FindLockerItem[CurrentLocker]):
                ResultSign.Show(Player, Msg("Locker holds: {ItemName}"))
            else:
                ResultSign.Show(Player, Msg("That locker is empty!"))
            # advance to the next locker for the next visitor, wrapping around
            set CurrentLocker = Mod[CurrentLocker + 1, LockerContents.Length]

    # THE OPTION-RETURNING FUNCTION.
    # Returns option{name} if the locker exists AND is non-empty; false otherwise.
    FindLockerItem(Index : int) : ?string =
        if (Name := LockerContents[Index], Name <> ""):
            return option{Name}
        return false

Line by line:

  • @editable DockPlate : trigger_device — the placed device we subscribe to. You MUST declare placed devices as @editable fields, or calling them fails with "Unknown identifier."
  • LockerContents : []string — our data. Position in the array is the locker number; "" marks an empty locker.
  • Msg<localizes>(S:string):message — the Show method takes a message (localized text), not a raw string. This helper wraps a string into a message. There is no StringToMessage.
  • DockPlate.TriggeredEvent.Subscribe(OnDockStep) — in OnBegin we hook the plate's real event to our handler method.
  • OnDockStep(Agent : ?agent) — the trigger's event hands an ?agent. if (Player := Agent?) unwraps it; inside the if block Player is a real agent.
  • if (ItemName := FindLockerItem[CurrentLocker]) — we call our option-returning function. Because it returns ?string, using it in an if unwraps it in one move: the block runs only when a value was present, and ItemName is a plain string there.
  • FindLockerItem(Index:int):?string — the return type ?string says "maybe a string." Inside, if (Name := LockerContents[Index], Name <> "") both indexes safely (indexing an array can fail) and checks it's non-empty. On success we return option{Name}; otherwise we fall through to return false.
  • Mod[...] wraps our counter so it cycles through the lockers.

The whole point: FindLockerItem reports honestly. It never returns a fake value for "nothing here" — it returns false, and the caller decides what to do.

Common patterns

Pattern 1 — Return the FIRST match (search that can miss). A classic option use: scan an array and return option{index} on a hit, false if never found.

treasure_finder := class(creative_device):

    @editable SearchButton : button_device = button_device{}
    @editable Sign : hud_message_device = hud_message_device{}

    var Beach : []string = array{"sand", "shell", "treasure", "crab"}

    Msg<localizes>(S : string) : message = "{S}"

    OnBegin<override>()<suspends>:void =
        SearchButton.InteractedWithEvent.Subscribe(OnSearch)

    OnSearch(Agent : agent) : void =
        # FindIndex returns ?int — option{i} if found, false otherwise
        if (Idx := FindIndex[Beach, "treasure"]):
            Sign.Show(Agent, Msg("Treasure at index {Idx}!"))
        else:
            Sign.Show(Agent, Msg("No treasure on this shore."))

    FindIndex(Items : []string, Target : string) : ?int =
        for (I := 0..Items.Length - 1):
            if (Items[I] = Target):
                return option{I}
        return false

Pattern 2 — Unwrap an option event payload with Agent?. Every listenable(?agent) event (like a trigger's) hands you an ?agent. Unwrapping it IS returning-an-option in reverse — you read what someone else returned.

cove_greeter := class(creative_device):

    @editable ShorePlate : trigger_device = trigger_device{}
    @editable Welcome : hud_message_device = hud_message_device{}

    Msg<localizes>(S : string) : message = "{S}"

    OnBegin<override>()<suspends>:void =
        ShorePlate.TriggeredEvent.Subscribe(OnShore)

    OnShore(MaybeAgent : ?agent) : void =
        # MaybeAgent is ?agent. Only greet if a real agent is inside.
        if (Player := MaybeAgent?):
            Welcome.Show(Player, Msg("Welcome to the cove!"))

Pattern 3 — Build an option in a variable over time (var ?T). You can hold an option in a variable and fill it in only when a search succeeds — great for "remember the last player who succeeded."

last_visitor_tracker := class(creative_device):

    @editable CheckIn : button_device = button_device{}
    @editable Announce : hud_message_device = hud_message_device{}

    # Starts empty: no visitor yet
    var LastVisitor : ?agent = false

    Msg<localizes>(S : string) : message = "{S}"

    OnBegin<override>()<suspends>:void =
        CheckIn.InteractedWithEvent.Subscribe(OnCheckIn)

    OnCheckIn(Agent : agent) : void =
        # If we already had a visitor, welcome them back first
        if (Prev := LastVisitor?):
            Announce.Show(Agent, Msg("A visitor was already here."))
        # Now store the current agent as an option
        set LastVisitor = option{Agent}
        Announce.Show(Agent, Msg("Checked in!"))

Gotchas

  • option{X} vs X. option{5} has type ?int; 5 has type int. To go from an option back to the value you must unwrap: if (V := MyOption?):. You cannot use a ?int where an int is expected.
  • false is the empty option. An empty option is literally written false (not option{} with nothing). return false from a function whose return type is ?T means "nothing found."
  • Unwrapping happens in a failable context. Agent? and MyOption? can fail, so they must live inside an if, for, or a []-decides function. Writing Player := Agent? at plain statement level won't compile — wrap it in if (Player := Agent?):.
  • Array indexing is itself failable. LockerContents[Index] can fail if Index is out of range, which is why it goes inside the if (Name := LockerContents[Index], ...). Combining conditions with commas short-circuits safely.
  • message is not a string. Device methods like Show want a message. Use a <localizes> helper (Msg("...")) — there is no StringToMessage function.
  • Verse never auto-converts int↔float. If your option holds an int and a method wants a float, unwrap then convert explicitly.

Guides & scripts that use trigger_device

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

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