Overview
When a player steps onto your clifftop dock or wades into a glowing cove, you often want to flash a contextual message — "Dive in!" or "Treasure found!" — then clear it when they leave. The player_ui class (from /UnrealEngine.com/Temporary/UI) is the main interface for adding and removing widget objects from a single player's screen.
The key methods are:
AddWidget(Widget)— pushes a widget onto the player's HUD.RemoveWidget(Widget)— pulls it back off cleanly.
Because widgets are per-player, you must track which widget belongs to which player. A [player]?text_block map is the idiomatic pattern: the player is the key, the optional text block is the value. When you need to remove it, you look up the player's widget and call RemoveWidget.
Reach for this pattern whenever you need:
- Contextual HUD prompts that appear and disappear based on zone entry/exit.
- Countdown timers or score popups that clear after a delay.
- Tutorial hints that dismiss once the player acts.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: A sunny dock sits at the edge of a cove. When a player walks onto a trigger plate on the dock, a cel-shaded "🌊 Dive In!" message appears on their screen. When they walk off the plate (a second trigger), the message is removed. Two trigger_devices — one for entry, one for exit — drive the whole thing.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Colors }
using { /UnrealEngine.com/Temporary/SpatialMath }
# Localizer helper — Verse requires `message` params, not raw strings
dock_message<localizes>(S : string) : message = "{S}"
dock_hud_device := class(creative_device):
# Place a trigger on the dock surface (player walks ON)
@editable DockEntryTrigger : trigger_device = trigger_device{}
# Place a trigger at the water's edge (player walks OFF / dives)
@editable DockExitTrigger : trigger_device = trigger_device{}
# Per-player widget storage: key = player, value = optional text_block
var MaybeWidgetPerPlayer : [player]?text_block = map{}
OnBegin<override>()<suspends> : void =
# Subscribe to both triggers
DockEntryTrigger.TriggeredEvent.Subscribe(OnDockEntered)
DockExitTrigger.TriggeredEvent.Subscribe(OnDockExited)
# Called when a player steps onto the dock plate
OnDockEntered(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
if (P := player[A]):
# Build the text block with a drop-shadow for that cel-shaded pop
MyText := text_block:
DefaultText := dock_message("🌊 Dive In!")
DefaultShadowColor := NamedColors.Black
DefaultShadowOpacity := 0.8
DefaultShadowOffset := option{vector2{X := 2.0, Y := 2.0}}
# Add the widget to this player's UI
if (UI := GetPlayerUI[P]):
UI.AddWidget(MyText)
# Store it so we can remove it later
if (set MaybeWidgetPerPlayer[P] = option{MyText}) {}
# Called when the player leaves the dock / reaches the cove edge
OnDockExited(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
if (P := player[A]):
# Look up the stored widget for this player
if (StoredMaybeWidget := MaybeWidgetPerPlayer[P]):
if (W := StoredMaybeWidget?):
# Remove it from the player's screen
if (UI := GetPlayerUI[P]):
UI.RemoveWidget(W)
# Clear the map entry
if (set MaybeWidgetPerPlayer[P] = false) {}
Line-by-line explanation:
| Lines | What's happening |
|---|---|
dock_message<localizes> |
Verse's message type requires a <localizes> function — you cannot pass a raw string to DefaultText. |
var MaybeWidgetPerPlayer |
A mutable map keyed by player. The value is ?text_block (an option) so we can store "no widget yet" as false. |
DockEntryTrigger.TriggeredEvent.Subscribe(OnDockEntered) |
Hooks the trigger's event to our handler. The handler receives ?agent. |
if (A := MaybeAgent?) |
Unwraps the optional agent — triggers fire with ?agent, not agent. |
if (P := player[A]) |
Downcasts agent to player; fails gracefully if the agent is an NPC. |
text_block: initializer |
Sets DefaultShadowColor, DefaultShadowOpacity, and DefaultShadowOffset at construction time — these fields are read-only after init; use the Set* methods to change them later. |
GetPlayerUI[P] |
Returns ?player_ui; the if unwraps it. |
UI.AddWidget(MyText) |
Pushes the widget onto this player's HUD. |
set MaybeWidgetPerPlayer[P] = option{MyText} |
Stores the widget reference so OnDockExited can find it. |
UI.RemoveWidget(W) |
The core remove call — cleanly pulls the widget off the player's screen. |
set MaybeWidgetPerPlayer[P] = false |
Clears the map entry (false is the empty option in Verse). |
Common patterns
Pattern 1 — Timed auto-dismiss (add, wait, remove)
Show a "Treasure Found!" splash on the clifftop for 3 seconds, then auto-remove it without needing a second trigger.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Colors }
timed_splash_message<localizes>(S : string) : message = "{S}"
timed_splash_device := class(creative_device):
@editable TreasureTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
TreasureTrigger.TriggeredEvent.Subscribe(OnTreasureFound)
OnTreasureFound(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
if (P := player[A]):
if (UI := GetPlayerUI[P]):
Splash := text_block:
DefaultText := timed_splash_message("💎 Treasure Found!")
DefaultShadowColor := NamedColors.Yellow
DefaultShadowOpacity := 1.0
DefaultShadowOffset := option{vector2{X := 3.0, Y := 3.0}}
UI.AddWidget(Splash)
# Suspend for 3 seconds, then remove
Sleep(3.0)
UI.RemoveWidget(Splash)
Note:
OnTreasureFoundis called synchronously bySubscribe, butSleeprequires a suspending context. Spawn a new async task for the timed removal usingspawn{ TimedRemove(UI, Splash) }if you need to keep the handler non-suspending. The pattern above works when the handler itself is allowed to suspend (UEFN runs subscribed handlers in their own fiber).
Pattern 2 — Updating shadow style at runtime with SetShadowOpacity / SetShadowColor
After the widget is on screen, pulse its shadow to draw the player's eye — useful for a cove countdown.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Colors }
cove_alert<localizes>(S : string) : message = "{S}"
cove_pulse_device := class(creative_device):
@editable AlertTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
AlertTrigger.TriggeredEvent.Subscribe(OnAlert)
OnAlert(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
if (P := player[A]):
if (UI := GetPlayerUI[P]):
Alert := text_block:
DefaultText := cove_alert("⚠️ Storm Incoming!")
DefaultShadowColor := NamedColors.Red
DefaultShadowOpacity := 0.0
DefaultShadowOffset := option{vector2{X := 2.0, Y := 2.0}}
UI.AddWidget(Alert)
# Pulse opacity high → low twice, then remove
Alert.SetShadowOpacity(1.0)
Sleep(0.5)
Alert.SetShadowOpacity(0.2)
Sleep(0.5)
Alert.SetShadowOpacity(1.0)
Sleep(0.5)
Alert.SetShadowOpacity(0.2)
Sleep(0.5)
# Done — remove the widget
UI.RemoveWidget(Alert)
Pattern 3 — Reading shadow state before removing
Log the final shadow offset before clearing the widget, useful for debugging layout.
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Colors }
debug_label<localizes>(S : string) : message = "{S}"
debug_remove_device := class(creative_device):
@editable DebugTrigger : trigger_device = trigger_device{}
var MaybeDebugWidget : ?text_block = false
OnBegin<override>()<suspends> : void =
DebugTrigger.TriggeredEvent.Subscribe(OnDebugTrigger)
OnDebugTrigger(MaybeAgent : ?agent) : void =
if (A := MaybeAgent?):
if (P := player[A]):
if (UI := GetPlayerUI[P]):
if (Existing := MaybeDebugWidget?):
# Read shadow offset before removal
MaybeOffset := Existing.GetShadowOffset()
# Remove the widget cleanly
UI.RemoveWidget(Existing)
set MaybeDebugWidget = false
else:
# First trigger press — add the widget
W := text_block:
DefaultText := debug_label("Shore Marker")
DefaultShadowColor := NamedColors.Blue
DefaultShadowOpacity := 0.9
DefaultShadowOffset := option{vector2{X := 1.0, Y := 1.0}}
UI.AddWidget(W)
set MaybeDebugWidget = option{W}
Gotchas
1. DefaultText requires a message, not a string
You cannot write DefaultText := "Dive In!". Verse's text_block.DefaultText is typed message, which is a localization-safe type. Always declare a <localizes> helper:
myMsg<localizes>(S : string) : message = "{S}"
There is no StringToMessage function — the <localizes> function is the only bridge.
2. Default* fields are init-only — use Set* methods after construction
DefaultShadowColor, DefaultShadowOffset, and DefaultShadowOpacity are read at widget construction time only. To change them on a live widget, call SetShadowColor(...), SetShadowOffset(...), or SetShadowOpacity(...). Assigning to DefaultShadowColor after construction has no effect.
3. player_ui is per-player — never share a widget instance across players
AddWidget adds the widget to one player's UI. If two players step on the dock simultaneously, create two separate text_block instances and add each to its respective player_ui. Sharing a single widget instance between players is undefined behavior.
4. Unwrap ?agent before casting to player
Trigger events fire listenable(?agent). Your handler receives (MaybeAgent : ?agent). Always unwrap with if (A := MaybeAgent?) first, then downcast with if (P := player[A]). Skipping either step is a compile or runtime failure.
5. RemoveWidget on an already-removed widget is safe but track state anyway
Calling RemoveWidget on a widget that was never added, or was already removed, does not crash — but it does nothing. Tracking your map/option variable keeps your logic clean and prevents double-add bugs (adding the same widget twice stacks it).
6. GetPlayerUI returns an option — always unwrap it
GetPlayerUI[P] is a failable expression. Wrap every call in if (UI := GetPlayerUI[P]): or you will get a compile error about using a failable expression in a non-failable context.