Overview
When something goes wrong on your island — a trigger fires twice, a score never increments, a device seems to do nothing — you need a way to see what Verse is actually doing at runtime. Two complementary tools handle this:
Print(s : string)— the quickest tool in the shed. Writes a string to the Output Log (and briefly to the HUD in Play-In-Editor). Zero setup required.log/log_channel— the production-grade sibling. You declare a named channel class, create aloginstance bound to it, and call.Print()on that instance. Channel-tagged messages are easier to filter in a busy log, and you can control verbosity per channel.
Neither tool affects gameplay — they are pure observation instruments. Both are invisible to players in published experiences.
When to reach for each:
| Situation | Tool |
|---|---|
| Quick one-off sanity check | Print() |
| Multi-system island with lots of log noise | log + named log_channel |
| You want to filter by system in the Output Log | log + named log_channel |
| Shipping — neither shows to players | Either (both are dev-only) |
API Reference
(API surface could not be resolved for this device.)
Walkthrough
The Scenario: A sun-drenched cove. A wooden dock juts over turquoise water. When a player steps onto a pressure plate at the end of the dock, a trap device activates. You want to log every stage: device startup, the moment a player triggers the plate, and whether the trap actually fired.
This example uses:
Print()for the earliest startup message (before the log channel is warm)- A custom
log_channel+loginstance for all subsequent tagged messages - A
trigger_device(pressure plate) and atrap_devicewired together
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Simulation }
# ── Named log channel for the dock trap system ──────────────────────────────
dock_trap_log_channel := class(log_channel){}
# ── Main device ─────────────────────────────────────────────────────────────
dock_trap_device := class(creative_device):
# Wire these up in the UEFN Details panel
@editable
DockPlate : trigger_device = trigger_device{}
@editable
CoveTrap : trick_tile_device = trick_tile_device{}
# One log instance bound to our named channel
Logger : log = log{Channel := dock_trap_log_channel}
OnBegin<override>()<suspends> : void =
# Print() for the very first heartbeat — visible immediately
Print("[DockTrap] Device is awake — sunny cove ready!")
# Subscribe to the pressure plate
DockPlate.TriggeredEvent.Subscribe(OnDockPlateTriggered)
# Tagged log so we can filter in a busy Output Log
Logger.Print("[DockTrap] Subscribed to DockPlate.TriggeredEvent")
# Called when any agent steps on the dock plate
OnDockPlateTriggered(TriggeringAgent : ?agent) : void =
Logger.Print("[DockTrap] Plate triggered!")
if (A := TriggeringAgent):
# Agent confirmed — arm the trap
Logger.Print("[DockTrap] Valid agent on dock — activating cove trap!")
CoveTrap.Enable()
else:
Logger.Print("[DockTrap] Plate fired but no agent found — skipping trap.")```
**Line-by-line breakdown:**
1. `dock_trap_log_channel := class(log_channel){}` — declares a zero-field subclass of `log_channel`. The class *name* becomes your filter keyword in the Output Log.
2. `Logger : log = log{Channel := dock_trap_log_channel{}}` — creates a `log` instance at field scope, bound to our channel. Every `Logger.Print(...)` call is tagged `[dock_trap_log_channel]` in the log.
3. `Print("[DockTrap] Device is awake…")` — bare built-in `Print`, no channel, fires the moment `OnBegin` runs. Good for "am I even running?" checks.
4. `DockPlate.TriggeredEvent.Subscribe(OnDockPlateTriggered)` — hooks the plate. The event delivers `?agent` (an *option* agent).
5. `if (A := TriggeringAgent?):` — the mandatory option-unwrap. Without this, the compiler won't let you pass `TriggeringAgent` to `CoveTrap.Activate()`.
6. `CoveTrap.Activate(A)` — only fires when a real agent is confirmed; the else-branch logs the edge case so you notice it during playtesting.
## Common patterns
### Pattern 1 — Startup heartbeat with `Print()`
The simplest possible debug signal: prove your device loaded and `OnBegin` ran.
```verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
cove_heartbeat_device := class(creative_device):
OnBegin<override>()<suspends> : void =
# Bare Print — no channel, no setup, instant feedback
Print("[CoveHeartbeat] OnBegin fired — island is live!")
# Simulate some async work (e.g. waiting for a wave to start)
Sleep(3.0)
Print("[CoveHeartbeat] 3 seconds elapsed — wave incoming!")
Use this pattern when you just need to confirm execution order. The message appears in the Output Log and briefly on the HUD during PIE.
Pattern 2 — Named log_channel for a multi-system island
When your island has a treasure system, a wave system, and an NPC patrol all printing to the same log, named channels let you filter. Search lagoon_treasure_log_channel in the Output Log to see only treasure messages.
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Simulation }
# Declare a channel — one per logical system
lagoon_treasure_log_channel := class(log_channel){}
lagoon_treasure_device := class(creative_device):
@editable
TreasureButton : button_device = button_device{}
@editable
TreasureGranter : item_granter_device = item_granter_device{}
TreasureLog : log = log{Channel := lagoon_treasure_log_channel{}}
OnBegin<override>()<suspends> : void =
TreasureLog.Print("[Treasure] System initialised — lagoon chest armed.")
TreasureButton.InteractedWithEvent.Subscribe(OnTreasureButtonPressed)
OnTreasureButtonPressed(Agent : agent) : void =
TreasureLog.Print("[Treasure] Button pressed — granting loot!")
TreasureGranter.GrantItem(Agent)
TreasureLog.Print("[Treasure] GrantItem called for agent.")
Notice button_device.InteractedWithEvent delivers a non-optional agent directly — no unwrap needed here.
Pattern 3 — Logging a numeric game state with string interpolation
String interpolation "{Value}" lets you embed variables directly into Print or Logger.Print messages — essential for tracking counters, scores, and timers.
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Simulation }
pirate_wave_log_channel := class(log_channel){}
pirate_wave_counter_device := class(creative_device):
@editable
WaveTrigger : trigger_device = trigger_device{}
WaveLog : log = log{Channel := pirate_wave_log_channel{}}
var WaveCount : int = 0
OnBegin<override>()<suspends> : void =
WaveLog.Print("[Waves] Pirate ship wave counter started.")
WaveTrigger.TriggeredEvent.Subscribe(OnWaveTriggered)
OnWaveTriggered(TriggeringAgent : ?agent) : void =
set WaveCount += 1
# Interpolate the int directly into the log string
WaveLog.Print("[Waves] Wave {WaveCount} has begun — brace the clifftop!")
if (WaveCount >= 3):
WaveLog.Print("[Waves] WARNING: 3+ waves reached — boss spawn threshold hit!")
"{WaveCount}" converts the int to its string representation inline. No cast function needed.
Gotchas
1. Print() is a dev tool, not a player UI
Print() writes to the Output Log and shows a brief on-screen message only in Play-In-Editor. It does NOT display to players in a published island. If you want in-game text, use a hud_message_device or notification_device instead.
2. You MUST subclass log_channel — you cannot instantiate the base
log{Channel := log_channel{}} will not compile because log_channel is abstract. Always declare your own my_channel := class(log_channel){} first.
3. TriggeredEvent delivers ?agent, not agent
The trigger_device.TriggeredEvent is a listenable(?agent). Your handler signature must be (TriggeringAgent : ?agent) and you must unwrap with if (A := TriggeringAgent?): before using the agent. Skipping this is the #1 compile error beginners hit.
4. String interpolation is your friend — but watch types
"{MyFloat}" and "{MyInt}" both work in interpolation. However, Verse does not auto-convert int to float or vice versa in arithmetic — only in string interpolation contexts. Keep your math types consistent.
5. log.Print vs built-in Print — same output, different filterability
Both write to the Output Log. The difference is that log.Print tags the line with your channel name, making it grep-able. On a large island with dozens of devices all calling bare Print(), the log becomes unreadable fast. Adopt named channels early.
6. Verse Debug Draw requires an Island Settings toggle
If you later graduate to visual debugging (debug_draw_channel), remember to enable Verse Debug Draw in Island Settings → Debug tab. Debug draw shapes are also invisible in published experiences.