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 (holdsX)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@editablefields, 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— theShowmethod takes amessage(localized text), not a raw string. This helper wraps a string into amessage. There is noStringToMessage.DockPlate.TriggeredEvent.Subscribe(OnDockStep)— inOnBeginwe 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 theifblockPlayeris a realagent.if (ItemName := FindLockerItem[CurrentLocker])— we call our option-returning function. Because it returns?string, using it in anifunwraps it in one move: the block runs only when a value was present, andItemNameis a plainstringthere.FindLockerItem(Index:int):?string— the return type?stringsays "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 wereturn option{Name}; otherwise we fall through toreturn 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}vsX.option{5}has type?int;5has typeint. To go from an option back to the value you must unwrap:if (V := MyOption?):. You cannot use a?intwhere anintis expected.falseis the empty option. An empty option is literally writtenfalse(notoption{}with nothing).return falsefrom a function whose return type is?Tmeans "nothing found."- Unwrapping happens in a failable context.
Agent?andMyOption?can fail, so they must live inside anif,for, or a[]-decides function. WritingPlayer := Agent?at plain statement level won't compile — wrap it inif (Player := Agent?):. - Array indexing is itself failable.
LockerContents[Index]can fail ifIndexis out of range, which is why it goes inside theif (Name := LockerContents[Index], ...). Combining conditions with commas short-circuits safely. messageis not astring. Device methods likeShowwant amessage. Use a<localizes>helper (Msg("...")) — there is noStringToMessagefunction.- Verse never auto-converts int↔float. If your option holds an
intand a method wants afloat, unwrap then convert explicitly.