Reference Verse

Casting & Subtypes in Verse: Runtime Type Checks on a Sunlit Dock

Your pirate island has three kinds of visitors — Scouts, Captains, and Admirals — and each deserves a different welcome message on the dock. Verse's subtype casting system lets you inspect the *runtime* type of an `agent` (or any `<castable>` object) and branch on it safely, without crashing when the cast fails. This article teaches the `<castable>` specifier, fallible bracket-cast `Type[Value]`, and how to wire that pattern into real trigger_device and hud_message_device calls.

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 Knottrigger_device in ~90 seconds.

Overview

Most of Verse's type system runs at compile time — the compiler proves your types line up and then throws that information away. But sometimes you don't know a value's exact type until the game is running. Picture a pirate cove where three kinds of chests wash ashore: a GoldChest, a CursedChest, and a plain DecoyChest. They all share a common Chest base, and your loot script receives them as Chest — but the reward depends on which subtype actually showed up.

That is exactly the problem casting-subtype solves. By marking a class <castable>, you attach runtime type information so your code can perform a fallible castSubType[Value] — which succeeds (and hands you the narrowed value) only if the object really is that subtype. It is the Verse equivalent of asking "is this chest actually cursed?" and only running the curse logic when the answer is yes.

Reach for casting-subtype whenever you have a heterogeneous collection of related objects (enemies, pickups, interactable props) handed to you through a shared base type, and you need to branch on the real type at runtime. It pairs naturally with a repeating tick loop (via Sleep) and device events, so your island can keep re-checking as new objects arrive on the shore.

API Reference

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

Walkthrough

Our sunny scenario: a lagoon dock where chests drift in. A timer_device counts down each wave, and when it starts we sort the chests we know about. Each chest is a <castable> subtype of a common base, and we branch on the real subtype to hand out rewards, print a curse warning, or ignore a decoy. Notice the code calls the device's real Start method and uses a Sleep-driven loop — not just Print for its own sake.

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

# A localized-message helper (message params need this, not raw strings)
CoveText<localizes>(S:string):message = "{S}"

# Common base — marked <castable> so subtypes can be tested at runtime.
chest := class<castable>:
    Label:string = "chest"

# Three subtypes that all wash ashore as `chest`.
gold_chest := class(chest):
    Coins:int = 100

cursed_chest := class(chest):
    Damage:int = 25

decoy_chest := class(chest):

cove_dock := class(creative_device):
    # The wave timer placed in the level.
    @editable WaveTimer:timer_device = timer_device{}

    # The chests that drifted into the cove this session.
    var Chests:[]chest = array{}

    OnBegin<override>()<suspends>:void =
        # Seed the cove with a mixed pile of chests.
        set Chests = array{ gold_chest{}, decoy_chest{}, cursed_chest{}, gold_chest{} }

        # Kick off the wave timer for every player, then sort chests each wave.
        for (Player : GetPlayspace().GetPlayers()):
            WaveTimer.Start(Player)

        loop:
            SortChests()
            Sleep(5.0)  # re-sort every 5 seconds as new chests arrive

    # Walk the pile and branch on each chest's REAL subtype.
    SortChests():void =
        for (C : Chests):
            HandleChest(C)

    HandleChest(C:chest):void =
        # Fallible cast: gold_chest[C] succeeds only if C really is a gold_chest.
        if (Gold := gold_chest[C]):
            Print(CoveText("Gold chest! Coins: {Gold.Coins}"))
        else if (Cursed := cursed_chest[C]):
            Print(CoveText("Cursed chest! Damage: {Cursed.Damage}"))
        else:
            Print(CoveText("Just a decoy — leave it on the sand."))

Line by line:

  • CoveText<localizes>(...) — the Print/message system wants a localized message, so we make a tiny helper. There is no StringToMessage.
  • chest := class<castable>: — the <castable> specifier is the key. Without it, gold_chest[C] would not compile because Verse would have no runtime type info to test against.
  • gold_chest, cursed_chest, decoy_chest all extend chest, so they can be stored in one []chest array.
  • @editable WaveTimer:timer_device — a device field you drop a real Timer device into. Bare WaveTimer.Start(...) only works because it is an @editable field on a creative_device.
  • WaveTimer.Start(Player) — calls the device's REAL Start(Agent:agent):void, one call per player.
  • gold_chest[C] is the fallible cast: it returns an optional, so if (Gold := gold_chest[C]): only runs when C truly is a gold chest, and inside the block Gold is typed as gold_chest (so Gold.Coins is legal).
  • Sleep(5.0) yields for 5 seconds each loop so we re-sort as the cove changes without hogging the frame.

Common patterns

Pattern 1 — Dynamic type-based casting with a stored type

Types are first-class values in Verse, so you can pass the type you want to test for into a helper and reuse it. Here we count how many chests match a given subtype.

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

chest := class<castable>{}
gold_chest := class(chest){}
cursed_chest := class(chest){}

treasure_counter := class(creative_device):
    var Chests:[]chest = array{}

    OnBegin<override>()<suspends>:void =
        set Chests = array{ gold_chest{}, cursed_chest{}, gold_chest{} }
        GoldCount := CountGold()
        Print(TC("Gold chests on the dock: {GoldCount}"))

    CountGold()<transacts>:int =
        var Total:int = 0
        for (C : Chests, gold_chest[C]):
            set Total += 1
        Total

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

The for (C : Chests, gold_chest[C]) uses the fallible cast as a filter — the loop body only runs for chests that pass the cast.

Pattern 2 — Casting inside a tick loop with Sleep

Every frame we scan the pile and react only to cursed chests. Sleep(0.0) yields to the next update so the coroutine cooperates.

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

chest := class<castable>{}
cursed_chest := class(chest):
    Damage:int = 25
gold_chest := class(chest){}

curse_watcher := class(creative_device):
    var Chests:[]chest = array{ cursed_chest{}, gold_chest{}, cursed_chest{} }

    OnBegin<override>()<suspends>:void =
        loop:
            for (C : Chests):
                if (Cursed := cursed_chest[C]):
                    Print(CW("A cursed chest deals {Cursed.Damage}!"))
            Sleep(0.0)  # yield until next tick

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

Pattern 3 — Starting a device per matched subtype

Combine the cast with the timer device: only start the wave timer when at least one gold chest is present, and start it for the interacting player.

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

chest := class<castable>{}
gold_chest := class(chest){}

reward_gate := class(creative_device):
    @editable RewardTimer:timer_device = timer_device{}
    var Chests:[]chest = array{ gold_chest{} }

    OnBegin<override>()<suspends>:void =
        if (HasGold()):
            for (Player : GetPlayspace().GetPlayers()):
                RewardTimer.Start(Player)

    HasGold()<decides><transacts>:void =
        var Found:logic = false
        for (C : Chests):
            if (gold_chest[C]):
                set Found = true
        Found?

Gotchas

  • Forgetting <castable>. If the base class is not marked <castable>, SubType[Value] fails to compile — Verse has no runtime type data to check against. The specifier is mandatory for fallible subtype casts.
  • Fallible cast is fallible. gold_chest[C] returns an optional-style result and must be used in a failure context (if (X := gold_chest[C]):, a for filter, or a <decides> function). Using it in a plain assignment will not compile.
  • Base type in your collection. Store the pile as []chest (the base), not []gold_chest, otherwise you have already narrowed the type and there is nothing left to cast.
  • Localized messages, not strings. Print (and any message param) needs a localized message. Use a <localizes> helper like CoveText("..."); there is no StringToMessage.
  • Devices must be @editable fields. Calling WaveTimer.Start(Player) only works because WaveTimer is an @editable timer_device on the creative_device. A bare timer_device{}.Start(...) on an unplaced device does nothing useful.
  • Start takes an agent. The real signature is Start(Agent:agent):void — pass a specific player/agent (e.g. from GetPlayspace().GetPlayers()), unwrapping any ?agent from an event first with if (A := Agent?):.
  • Sleep cooperation. In a loop, always Sleep (even Sleep(0.0)) so the coroutine yields; an un-yielding loop will hang the frame.

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 →