Reference Verse compiles

map Keys Are Unique: Tracking Players on a Pirate Cove Island

Every key in a Verse `map` is unique — if you insert the same key twice, the last value silently wins. On a sun-drenched pirate cove island, that guarantee becomes a superpower: track which treasure chests a player has already looted, count zone visits, or deduplicate trigger events without writing a single duplicate-check loop. This article teaches you the map-keys-unique contract through a real UEFN scenario and shows you how to wire it to `trigger_device` and `hud_message_device` so the islan

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

Overview

A Verse map is a container of key-value pairs where every key must be unique. When you write a map literal with duplicate keys, only the last value is kept — earlier entries are silently overwritten:

WordCount:[string]int = map{
    "apple" => 0,
    "apple" => 1,
    "apple" => 2   # only this survives; WordCount["apple"] = 2
}

This uniqueness contract is enforced at compile time for literals and at runtime for dynamic updates. It means you can use a map as a deduplication ledger: once a key exists, you know that thing has happened exactly once (or N times, if the value is a counter).

When to reach for it:

  • Track which players have stepped on a trigger (each agent key is unique).
  • Count how many times each zone has been visited without worrying about duplicate entries.
  • Store per-player state (score, flag, status) keyed by agent.
  • Deduplicate events fired by multiple overlapping trigger devices.

Types that CAN be map keys: int, float, string, bool, logic, enums, <unique> classes, and agent. Types that CANNOT: plain classes (without <unique>), interfaces (without <unique>), function types, type, false.

API Reference

trigger_device

Used to relay events to other linked devices.

Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse). Inherited members are merged from trigger_base_device.

trigger_device<public> := class<concrete><final>(trigger_base_device):

Events (subscribe a handler to react):

Event Signature Description
TriggeredEvent TriggeredEvent<public>:listenable(?agent) Signaled when an agent triggers this device. Sends the agent that used this device. Returns false if no agent triggered the action (ex: it was triggered through code).

Methods (call these to make the device act):

Method Signature Description
Trigger Trigger<public>(Agent:agent):void Triggers this device with Agent being passed as the agent that triggered the action. Use an agent reference when this device is setup to require one (for instance, you want to trigger the device only with a particular agent.
Trigger Trigger<public>():void Triggers this device, causing it to activate its TriggeredEvent event.
Enable Enable<public>():void Enables this device.
Disable Disable<public>():void Disables this device.
SetMaxTriggerCount SetMaxTriggerCount<public>(MaxCount:int):void Sets the maximum amount of times this device can trigger. * 0 can be used to indicate no limit on trigger count. * MaxCount is clamped between [0,20].
GetMaxTriggerCount GetMaxTriggerCount<public>()<transacts>:int Gets the maximum amount of times this device can trigger. * 0 indicates no limit on trigger count.
GetTriggerCountRemaining GetTriggerCountRemaining<public>()<transacts>:int Returns the number of times that this device can still be triggered before hitting GetMaxTriggerCount. Returns 0 if GetMaxTriggerCount is unlimited.
SetResetDelay SetResetDelay<public>(Time:float):void Sets the time (in seconds) after triggering, before the device can be triggered again (if MaxTrigger count allows).
GetResetDelay GetResetDelay<public>()<transacts>:float Gets the time (in seconds) before the device can be triggered again (if MaxTrigger count allows).
SetTransmitDelay SetTransmitDelay<public>(Time:float):void Sets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered.
GetTransmitDelay GetTransmitDelay<public>()<transacts>:float Gets the time (in seconds) which must pass after triggering, before this device informs other external devices that it has been triggered.

hud_message_device

Used to show custom HUD messages to one or more agents.

Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).

hud_message_device<public> := class<concrete><final>(creative_device_base):

Events (subscribe a handler to react):

Event Signature Description
ShowMessageEvent ShowMessageEvent<public>:listenable(agent) Called when a Message has been Shown on-screen. Returns an Agent if it was Shown on a specified Agent's screen.
HideMessageEvent HideMessageEvent<public>:listenable(agent) Called when a Message has been Hidden on-screen. Returns an Agent if it was Hidden from a specified Agent's screen.
ClearAllMessagesEvent ClearAllMessagesEvent<public>:listenable(agent) Called when all queued Messages from all players that are affected by this HUD Message Device have been cleared.

Methods (call these to make the device act):

Method Signature Description
Show Show<public>(Agent:agent):void Shows the currently set HUD Message on Agents screen. Will replace any previously active message. Use this when the device is setup to target specific agents.
Show Show<public>():void Shows the currently set Message HUD message on screen. Will replace any previously active message.
Hide Hide<public>():void Hides the HUD message.
Hide Hide<public>(Agent:agent):void Hides the currently set HUD Message on Agents screen. Use this when the device is setup to target specific agents.
Show Show<public>(Agent:agent, Message:message, ?DisplayTime:float Displays a Custom message to a specific Agent that you define.Setting DisplayTime to 0.0 will display the HUD message persistently.If not defined, or less than 0.0 the message will show for the time set on the device.
Show Show<public>(Message:message, ?DisplayTime:float Displays a Custom message that you define for all PlayersSetting DisplayTime to 0.0 will display the HUD message persistently.If not defined, or less than 0.0 the message will show for the time set on the device.
SetDisplayTime SetDisplayTime<public>(Time:float):void Sets the time (in seconds) the HUD message will be displayed. 0.0 will display the HUD message persistently.
GetDisplayTime GetDisplayTime<public>()<transacts>:float Returns the time (in seconds) for which the HUD message will be displayed. 0.0 means the message is displayed persistently.
SetText SetText<public>(Text:message):void Sets the Message to be displayed when the HUD message is activated. Text is clamped to 150 characters.
ClearAllMessages ClearAllMessages<public>():void Clears all queued Messages from all players that are affected by this HUD Message Device.

Walkthrough

Scenario: Pirate Cove — Three Treasure Triggers, One HUD Scoreboard

Your island has three buried-treasure spots along the cove shore. Each spot has a trigger_device (players walk over it to "dig up" the chest). A hud_message_device shows the crew how many unique chests each player has found. Because a player might accidentally walk over the same trigger twice, you use a [agent][int] map to count unique discoveries per player — the map-key guarantee means each player key only ever appears once, and you update the integer value to track their running total.

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

# Localized message helpers
FoundChest<localizes>(N:int):message = "Treasure found! You have {N} chest(s) this run."
AlreadyFound<localizes>():message = "You already looted this chest!"
AllFound<localizes>():message = "LEGENDARY HAUL! All 3 chests found — you are the Pirate King!"

pirate_cove_treasure_manager := class(creative_device):

    # ── Devices placed on the island ──────────────────────────────────────────
    @editable ChestTrigger1 : trigger_device = trigger_device{}
    @editable ChestTrigger2 : trigger_device = trigger_device{}
    @editable ChestTrigger3 : trigger_device = trigger_device{}
    @editable ScoreHUD      : hud_message_device = hud_message_device{}

    # ── Per-player chest count: [agent]int ────────────────────────────────────
    # Keys are unique agents; value = number of UNIQUE chests found this session.
    var PlayerChestCount : [agent]int = map{}

    # Track which (player, chest-index) pairs have already been looted.
    # Key: agent  Value: set encoded as [int]int  (chest index => 1)
    var PlayerLootedChests : [agent][int]int = map{}

    OnBegin<override>()<suspends>:void =
        # Each trigger fires at most 3 times total (one per player visit is fine;
        # we deduplicate per-player in code). Reset delay = 0.5 s so rapid
        # re-entries don't spam the event.
        ChestTrigger1.SetMaxTriggerCount(0)   # 0 = unlimited
        ChestTrigger1.SetResetDelay(0.5)
        ChestTrigger2.SetMaxTriggerCount(0)
        ChestTrigger2.SetResetDelay(0.5)
        ChestTrigger3.SetMaxTriggerCount(0)
        ChestTrigger3.SetResetDelay(0.5)

        # Display time: 3 seconds per message
        ScoreHUD.SetDisplayTime(3.0)

        # Subscribe handlers
        ChestTrigger1.TriggeredEvent.Subscribe(OnChest1Triggered)
        ChestTrigger2.TriggeredEvent.Subscribe(OnChest2Triggered)
        ChestTrigger3.TriggeredEvent.Subscribe(OnChest3Triggered)

    # ── Shared logic ──────────────────────────────────────────────────────────
    HandleChestFound(MaybeAgent : ?agent, ChestIndex : int):void =
        # Unwrap the optional agent (trigger fires with ?agent)
        if (A := MaybeAgent?):
            # Ensure the player has a loot-record map; if not, create one.
            PlayerLooted := if (Existing := PlayerLootedChests[A]) then Existing else map{}

            # Check if this chest was already looted by this player.
            # map key uniqueness: if ChestIndex key exists, they already got it.
            if (PlayerLooted[ChestIndex] = 1):
                # Already looted — show a gentle reminder
                ScoreHUD.Show(A, AlreadyFound())
            else:
                # New chest! Record it. Build a new map with the chest index added.
                var NewPlayerLooted : [int]int = map{ChestIndex => 1}
                # Merge existing looted chests into the new map
                for (Idx -> Val : PlayerLooted):
                    set NewPlayerLooted = map{Idx => Val}
                set PlayerLootedChests = map{A => NewPlayerLooted}

                # Update the running total for this player
                PrevCount := if (C := PlayerChestCount[A]) then C else 0
                NewCount := PrevCount + 1
                set PlayerChestCount = map{A => NewCount}

                # Show score HUD
                if (NewCount >= 3):
                    ScoreHUD.Show(A, AllFound(), ?DisplayTime := 6.0)
                else:
                    ScoreHUD.Show(A, FoundChest(NewCount))

    # ── Per-trigger event handlers ────────────────────────────────────────────
    OnChest1Triggered(MaybeAgent : ?agent):void =
        HandleChestFound(MaybeAgent, 1)

    OnChest2Triggered(MaybeAgent : ?agent):void =
        HandleChestFound(MaybeAgent, 2)

    OnChest3Triggered(MaybeAgent : ?agent):void =
        HandleChestFound(MaybeAgent, 3)```

### Line-by-line explanation

| Lines | What's happening |
|---|---|
| `@editable` fields | Every device you want to call from Verse must be declared as an editable field on the class. You then wire them up in the UEFN editor. |
| `var PlayerChestCount : [agent]int` | A mutable map keyed by `agent`. Because keys are unique, each player appears at most once — no duplicate entries possible. |
| `SetMaxTriggerCount(0)` | `0` means unlimited triggers. We handle deduplication ourselves in the map. |
| `SetResetDelay(0.5)` | Half-second cooldown prevents event spam from a player standing on the trigger. |
| `SetDisplayTime(3.0)` | Tells the HUD device to auto-hide messages after 3 seconds. |
| `TriggeredEvent.Subscribe(OnChest1Triggered)` | Subscribes a class-scope method. The handler signature matches `listenable(?agent)`. |
| `if (A := MaybeAgent?)` | Unwraps the `?agent` — mandatory before you can use it as a map key. |
| `PlayerLooted[ChestIndex] = 1` | Fails (failable expression) if the key doesn't exist  that's how we detect "already looted". |
| `set PlayerChestCount = map{A => NewCount}` | Rebuilds the map with the updated value. Because `A` is unique, any previous entry for this agent is overwritten. |
| `ScoreHUD.Show(A, AllFound(), ?DisplayTime := 6.0)` | Shows a custom message to a specific agent with a custom display time override. |

## Common patterns

### Pattern 1 — Programmatic trigger + HUD broadcast (no agent required)

Sometimes you want to fire a trigger from code (e.g., a timed event) and broadcast a message to everyone. This pattern uses `Trigger()` (no agent) and `Show()` (no agent).

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

TideWarning<localizes>():message = "The tide is rising! Head for the clifftop!"

tide_alarm_manager := class(creative_device):

    @editable TideAlarmTrigger : trigger_device = trigger_device{}
    @editable TideHUD          : hud_message_device = hud_message_device{}

    # Track how many tide warnings have fired this session: [int]int
    # Key = wave number (unique), Value = timestamp placeholder (1)
    var WavesFired : [int]int = map{}
    var WaveNumber : int = 0

    OnBegin<override>()<suspends>:void =
        TideAlarmTrigger.SetMaxTriggerCount(5)   # max 5 tide warnings per game
        TideAlarmTrigger.SetTransmitDelay(1.0)   # 1-second delay before linked devices respond
        TideHUD.SetDisplayTime(4.0)

        # Subscribe to know when the trigger fires (even from code)
        TideAlarmTrigger.TriggeredEvent.Subscribe(OnTideAlarm)

        # Simulate tide rising every 30 seconds
        loop:
            Sleep(30.0)
            Remaining := TideAlarmTrigger.GetTriggerCountRemaining()
            if (Remaining > 0):
                TideAlarmTrigger.Trigger()   # fire with no agent

    OnTideAlarm(MaybeAgent : ?agent):void =
        # Record this wave; map key uniqueness ensures no duplicate wave numbers
        set WaveNumber = WaveNumber + 1
        set WavesFired = map{WaveNumber => 1}
        # Broadcast to all players — no agent version
        TideHUD.Show(TideWarning())

Pattern 2 — Enable/Disable a trigger based on map state, hide HUD per-player

This pattern shows Enable/Disable on the trigger and Hide(Agent) on the HUD — useful for a lagoon gate that locks after the first player passes through.

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

GateOpen<localizes>():message = "The lagoon gate is open — swim through!"
GateLocked<localizes>():message = "The gate has closed behind you."

lagoon_gate_manager := class(creative_device):

    @editable LagoonGateTrigger : trigger_device = trigger_device{}
    @editable GateHUD           : hud_message_device = hud_message_device{}

    # Map of agents who have passed through: [agent]logic
    # logic keys are unique per agent — once set to true, they passed.
    var PassedThrough : [agent]logic = map{}

    OnBegin<override>()<suspends>:void =
        # Only one player can open the gate per game session
        LagoonGateTrigger.SetMaxTriggerCount(1)
        LagoonGateTrigger.SetResetDelay(0.0)
        GateHUD.SetDisplayTime(0.0)   # persistent until hidden manually

        LagoonGateTrigger.TriggeredEvent.Subscribe(OnGateTriggered)

    OnGateTriggered(MaybeAgent : ?agent):void =
        if (A := MaybeAgent?):
            # Check map: if agent key already exists, they already passed
            if (PassedThrough[A]):
                GateHUD.Hide(A)   # hide any lingering message for this agent
            else:
                # Record passage — map key uniqueness: this agent is now in the ledger
                set PassedThrough = map{A => true}
                GateHUD.Show(A, GateOpen())
                # Disable the trigger so no one else can open it this session
                LagoonGateTrigger.Disable()
                Sleep(5.0)
                GateHUD.Show(A, GateLocked())
                Sleep(3.0)
                GateHUD.Hide(A)

Pattern 3 — ClearAllMessages and re-enable trigger for a new round

This pattern shows ClearAllMessages() and Enable() — handy for resetting the island between rounds.

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

RoundStart<localizes>(N:int):message = "Round {N} — find the hidden cove!"

cove_round_manager := class(creative_device):

    @editable RoundStartTrigger : trigger_device = trigger_device{}
    @editable RoundHUD          : hud_message_device = hud_message_device{}

    # Round number tracked in a map so each round key is unique
    var RoundLog : [int]int = map{}   # round number => 1
    var CurrentRound : int = 0

    OnBegin<override>()<suspends>:void =
        RoundStartTrigger.SetMaxTriggerCount(0)   # unlimited rounds
        RoundStartTrigger.SetResetDelay(2.0)
        RoundHUD.SetDisplayTime(5.0)

        RoundStartTrigger.TriggeredEvent.Subscribe(OnRoundStart)

    OnRoundStart(MaybeAgent : ?agent):void =
        # Clear all lingering HUD messages from the previous round
        RoundHUD.ClearAllMessages()

        # Advance round counter; map key uniqueness records each round once
        set CurrentRound = CurrentRound + 1
        set RoundLog = map{CurrentRound => 1}

        # Broadcast new round message to all players
        RoundHUD.Show(RoundStart(CurrentRound))

        # Re-enable the trigger in case it was disabled by another system
        RoundStartTrigger.Enable()

Gotchas

1. Duplicate keys silently overwrite — this is the feature, not a bug

When you write set MyMap = map{A => NewValue} for an agent key that already exists, the old value is replaced. This is exactly what makes maps useful as per-player ledgers. But if you accidentally rebuild a map without carrying over existing entries, you will lose data. Always iterate over the old map and merge when you need to preserve prior entries.

2. Accessing a missing key is failable — always guard it

# WRONG — crashes if A has no entry yet
Count := PlayerChestCount[A]

# CORRECT — use if/else to provide a default
Count := if (C := PlayerChestCount[A]) then C else 0

3. ?agent must be unwrapped before use as a key

TriggeredEvent is a listenable(?agent). The ?agent is an option type — you cannot use it directly as a map key. Always unwrap first:

OnTriggered(MaybeAgent : ?agent):void =
    if (A := MaybeAgent?):   # A is now agent, safe to use as map key
        set MyMap = map{A => 1}

4. message parameters require localized values — not raw strings

Methods like Show(Agent, Message:message, ...) and SetText(Text:message) take message, not string. Declare a localizes helper:

MyLabel<localizes>(S:string):message = "{S}"
# Then pass: ScoreHUD.Show(A, MyLabel("Hello!"))

There is no StringToMessage function in Verse.

5. SetMaxTriggerCount clamps between 0 and 20

Passing a value outside [0, 20] is silently clamped. Use 0 for unlimited. If you need more than 20 discrete trigger events, manage the count yourself in a map.

6. GetTriggerCountRemaining returns 0 when the limit is unlimited

If GetMaxTriggerCount() returns 0 (unlimited), then GetTriggerCountRemaining() also returns 0 — not a large number. Check GetMaxTriggerCount() first to distinguish "unlimited" from "exhausted".

7. Verse does not auto-convert intfloat

SetResetDelay and SetDisplayTime take float. Write 0.5 not 0 or you will get a type error. Similarly, SetMaxTriggerCount takes int — pass 3 not 3.0.

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 →