Reference Devices compiles

No Null in Verse: Failable Values with the Player Reference Device

Coming from C# or C++? There is no `null` in Verse. Instead of a value that might secretly be nothing and crash you at runtime, Verse uses option types (`?type`), the `false` outcome, and `<decides>` expressions to make 'no value' something the compiler forces you to handle. This article teaches that model by wiring it up to a real device — the Player Reference device — whose `IsReferenced`, `GetAgent`, and `GetStatValue` calls all lean on Verse's no-null safety.

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

Overview

If you have programmed before, you have been burned by null — a pointer that was supposed to hold a player, an item, or a device, but held nothing, and blew up the moment you touched it. Verse does not have null. There is no null reference, no nil, no None you can silently pass around.

Instead Verse makes "there might be no value here" a thing the compiler tracks for you, using three related tools:

  • Option types — written ?agent, ?int, ?player, etc. An option either holds a value or is false. You cannot use the inner value until you unwrap it with ?.
  • The false outcome — expressions marked <decides> either succeed and produce a value, or fail (produce nothing). A failing expression never returns garbage; control simply skips the branch.
  • if / for context — the only places you are allowed to run a failable expression, so "what if there is no value?" is always answered right where you ask the question.

The game problem this solves: "Is THIS player the one my device cares about? And if so, what's their score?" We answer that with the Player Reference device, which exposes exactly the calls that make Verse's no-null design concrete: IsReferenced(Agent) (a <decides> test that fails instead of returning false-null), GetAgent() (a <decides> call that fails if there is no referenced agent), and GetStatValue() (returns an int, never a null-int). Reach for this pattern any time a device or event hands you a maybe value.

API Reference

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

Walkthrough

Scenario: a VIP checkpoint. A trigger fires when any player steps on it. We only want to react if the stepping player is the one tracked by our Player Reference device — the "VIP". If they are, we read their tracked stat (say, a score) and only unlock a reward if it's high enough. Every step here is a place where a lesser language would have handed you a null; Verse hands you a failable expression instead.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

vip_checkpoint := class(creative_device):

    # A trigger the player steps on.
    @editable
    Checkpoint : trigger_device = trigger_device{}

    # The Player Reference device that tracks our VIP.
    @editable
    VIPReference : player_reference_device = player_reference_device{}

    # We open this door only for a qualifying VIP.
    @editable
    RewardDoor : barrier_device = barrier_device{}

    # Score the VIP must beat.
    RequiredScore : int = 100

    OnBegin<override>()<suspends> : void =
        # Subscribe our method to the trigger. The handler receives ?agent.
        Checkpoint.TriggeredEvent.Subscribe(OnStepped)

    # Event handlers are METHODS at class scope.
    OnStepped(MaybeAgent : ?agent) : void =
        # 1. Unwrap the option. If it's empty, this branch is skipped -
        #    there is no null agent to accidentally use.
        if (Stepper := MaybeAgent?):
            # 2. IsReferenced is <decides>: it SUCCEEDS only when this is
            #    the tracked player. On failure the if-branch never runs.
            if (VIPReference.IsReferenced[Stepper]):
                CheckAndReward()

    CheckAndReward() : void =
        # 3. GetAgent is not fallible, it just returns the tracked agent.
        VIP := VIPReference.GetAgent()
        # 4. GetStatValue returns a plain int - never null.
        Score := VIPReference.GetStatValue()
        # 5. int comparison, no int/float mixing.
        if (Score >= RequiredScore):
            RewardDoor.Disable()   # open the door```

Line by line:

- `@editable ... = trigger_device{}`  every placed device you touch must be an `@editable` field. A bare `Checkpoint.TriggeredEvent` with no field would fail with *Unknown identifier*.
- `Checkpoint.TriggeredEvent.Subscribe(OnStepped)`  we bind in `OnBegin`. `TriggeredEvent` is a `listenable(?agent)`, so the handler is typed `(?agent)`.
- `if (Stepper := MaybeAgent?)`  the `?` unwraps the option. If the trigger fired without an agent, `MaybeAgent?` *fails* and the whole `if` is skipped. There is no way to reach the body with an empty agent.
- `if (VIPReference.IsReferenced[Stepper])`  note the **square brackets**: `IsReferenced` is `<decides>`, so it's called in a failure context. It doesn't return `true`/`false`/`null`; it either succeeds (this IS the referenced player) or fails.
- `if (VIP := VIPReference.GetAgent[])`  also `<decides>`; fails cleanly if the device currently references nobody. Compare to null-returning getters in other languages.
- `GetStatValue()`  ordinary call, returns `int`. No decides, no option, because an int always exists.
- `RewardDoor.Disable()`  the actual game effect: we drop the barrier so the VIP walks through.

## Common patterns

**Pattern 1  `GetAgent[]` failure is your "no player set" guard.** Instead of checking `player != null`, you let the `<decides>` call fail. Here we spawn a message-free effect: enable a beacon device only when a VIP actually exists.

```verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

vip_beacon := class(creative_device):

    @editable
    VIPReference : player_reference_device = player_reference_device{}

    @editable
    Beacon : beacon_device = beacon_device{}

    @editable
    PollButton : button_device = button_device{}

    OnBegin<override>()<suspends> : void =
        PollButton.InteractedWithEvent.Subscribe(OnPoll)

    OnPoll(MaybeAgent : ?agent) : void =
        # GetAgent[] fails (no null) when nobody is referenced yet.
        if (VIP := VIPReference.GetAgent[]):
            Beacon.Enable()
        else:
            Beacon.Disable()

Pattern 2 — GetStatValue() drives a scoreboard, no null-int. The stat getter always returns an int, so we can compare and branch without a null check. We update a HUD message with a localized value.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

score_gate := class(creative_device):

    @editable
    VIPReference : player_reference_device = player_reference_device{}

    @editable
    HUD : hud_message_device = hud_message_device{}

    @editable
    CheckButton : button_device = button_device{}

    # message params take a LOCALIZED value, not a raw string.
    ScoreText<localizes>(N : int) : message = "VIP score: {N}"

    OnBegin<override>()<suspends> : void =
        CheckButton.InteractedWithEvent.Subscribe(OnCheck)

    OnCheck(MaybeAgent : ?agent) : void =
        Score := VIPReference.GetStatValue()   # always an int
        HUD.SetText(ScoreText(Score))
        HUD.Show()

Pattern 3 — combine an option unwrap with IsReferenced[] for a two-stage filter. First unwrap the agent (no null), then test identity with the <decides> device call.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

vip_only_door := class(creative_device):

    @editable
    VIPReference : player_reference_device = player_reference_device{}

    @editable
    Plate : trigger_device = trigger_device{}

    @editable
    Door : barrier_device = barrier_device{}

    OnBegin<override>()<suspends> : void =
        Plate.TriggeredEvent.Subscribe(OnPlate)

    OnPlate(MaybeAgent : ?agent) : void =
        # Stage 1: unwrap the option (skips if empty).
        if (Stepper := MaybeAgent?):
            # Stage 2: identity test that fails instead of returning null.
            if (VIPReference.IsReferenced[Stepper]):
                Door.Disable()
            else:
                Door.Enable()

Gotchas

  • ? unwrap vs [] decides — they look similar but differ. MaybeAgent? unwraps an option. IsReferenced[Stepper] and GetAgent[] are <decides> calls (square brackets, failure context). Both must live inside an if (or for), because both can fail — that failure IS Verse's stand-in for null.

  • You cannot use a <decides> result outside a failure context. Writing VIP := VIPReference.GetAgent() with parentheses at top level won't compile — the call needs [] and an if. That's the language forcing you to answer "what if there's no agent?".

  • GetStatValue() is NOT failable. It returns a plain int, so don't wrap it in if. There is no "null int" to guard against; if no stat is tracked it returns the device's current tracked value (often 0).

  • message params need localized text. HUD.SetText("hi") fails — a raw string isn't a message. Declare MyText<localizes>(...) : message = "..." and pass MyText(...). There is no StringToMessage.

  • No int↔float auto-convert. If you compare GetStatValue() (an int) to a float threshold, convert explicitly (e.g. Int[SomeFloat]) — Verse never silently coerces.

  • Every device is an @editable field. Referencing a placed device that isn't declared as a field gives Unknown identifier — the most common beginner error, unrelated to null but just as fatal.

Build your own lesson with no_null_in_verse

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 →