Overview
When you're building a Fortnite island in UEFN, your Verse scripts can grow fast — trigger subscriptions, stat checks, AI focus calls, conditional unlocks. Comments are the tool that keeps that complexity navigable. They are completely ignored at compile time and runtime; they exist purely for humans.
Verse supports three comment forms:
| Style | Syntax | Best for |
|---|---|---|
| Single-line | # comment |
Quick notes, TODO markers, disabling a line |
| Inline block | <# comment #> |
Labelling a value mid-expression without breaking the line |
| Multi-line block | <# … #> spanning multiple lines |
Algorithm explanations, section headers, function contracts |
Beyond syntax, readability is about naming, structure, and intent. A well-named variable (VaultIsUnlocked instead of B) often makes a comment unnecessary. A short comment above a subscription call explains why, not just what.
Use this article as your reference whenever you add a new device, subscribe to an event, or write logic that will be read by anyone other than you — including future-you.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: a vault door that tracks how many players have stepped on pressure plates. When the count reaches a threshold, the vault opens and a weapon is granted. Every meaningful decision in the script is documented with the appropriate comment style.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SpatialMath }
<#
vault_controller — Verse Island example
Tracks players stepping on two pressure plates.
When both plates are active simultaneously the vault
trigger fires and a weapon granter rewards the team.
Devices to place in UEFN:
- Two trigger_device props acting as pressure plates
- One trigger_device for the vault door FX
- One item_granter_device for the weapon reward
#>
vault_controller := class(creative_device):
# ── Editable device references ──────────────────────────────────
# Plate A: left pressure plate trigger
@editable PlateA : trigger_device = trigger_device{}
# Plate B: right pressure plate trigger
@editable PlateB : trigger_device = trigger_device{}
# Fires when the vault door should open (drives a door animation)
@editable VaultTrigger : trigger_device = trigger_device{}
# Grants the vault weapon to the activating player
@editable WeaponGranter : item_granter_device = item_granter_device{}
# ── Internal state ───────────────────────────────────────────────
# Tracks which plates are currently pressed.
# Both must be true at the same time to open the vault.
var PlateAActive : logic = false
var PlateBActive : logic = false
# ── Lifecycle ────────────────────────────────────────────────────
OnBegin<override>()<suspends> : void =
<# Subscribe to both plates so we can track their independent states.
We use named methods (not lambdas) to keep OnBegin readable. #>
PlateA.TriggeredEvent.Subscribe(OnPlateATriggered)
PlateB.TriggeredEvent.Subscribe(OnPlateBTriggered)
# ── Event handlers ───────────────────────────────────────────────
# Called when a player steps on Plate A.
# Marks the plate active then checks the vault condition.
OnPlateATriggered(Agent : ?agent) : void =
set PlateAActive = true # latch: stays true until reset
CheckVaultCondition(Agent)
# Called when a player steps on Plate B.
OnPlateBTriggered(Agent : ?agent) : void =
set PlateBActive = true
CheckVaultCondition(Agent)
# Evaluates whether both plates are active.
# If so, fires the vault and grants the weapon to the triggering agent.
CheckVaultCondition(MaybeAgent : ?agent) : void =
if (PlateAActive? and PlateBActive?):
<# Both plates are live — open the vault door.
VaultTrigger drives a Sequencer cinematic via its
TriggeredEvent, so we just call Trigger() here. #>
VaultTrigger.Trigger()
# Unwrap the optional agent before granting the weapon.
# The granter requires a concrete agent, not an option type.
if (A := MaybeAgent?):
WeaponGranter.GrantItem(A)
<# Reset both latches so the vault cannot be
triggered a second time in the same round. #>
set PlateAActive = false
set PlateBActive = false
Line-by-line explanation
Multi-line block comment at the top (<# … #>) — acts as a file header. It names the device, describes the gameplay intent, and lists the UEFN devices a designer must place. This is the most important comment in any file: it answers what does this do and how do I set it up?
Section-divider single-line comments (# ── Editable device references ──) — visually group fields. Verse has no region keyword, so comment dividers are the standard way to create scannable sections.
Per-field single-line comments — each @editable field gets one line explaining its role. Short and specific beats long and vague.
Inline block comment (<# original amount #>) — used inside CheckVaultCondition to annotate the vault trigger call without breaking the expression onto a new line.
Intent comment on the latch (# latch: stays true until reset) — explains why the value is set to true and not toggled, preventing a future editor from "fixing" it.
Unwrap comment — the comment before if (A := MaybeAgent?) explains why we must unwrap: the granter API requires a concrete agent. This is the kind of comment that prevents a subtle bug.
Common patterns
Pattern 1 — Inline block comments to annotate a multi-part expression
Use <# … #> when you want to label the parts of a compound calculation without splitting it across lines. Here a stat tracker drives a score multiplier.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
<# score_display — shows how inline block comments document a formula #>
score_display := class(creative_device):
@editable StatTracker : tracker_device = tracker_device{}
OnBegin<override>()<suspends> : void =
# Read the raw stat value from the tracker device
RawScore : int = StatTracker.GetStatValue()
<# Multiply by 10 to convert "eliminations" into display points.
The tracker returns an int so no float conversion is needed. #>
DisplayPoints : int =
RawScore <# raw elimination count #>
* 10 <# points per elimination #>
# TODO: pipe DisplayPoints into a HUD element when UI API is available
_ = DisplayPoints # suppress unused-variable warning during prototyping
Pattern 2 — TODO and FIXME markers with single-line comments
Single-line # comments are the standard place for TODO, FIXME, and NOTE markers. They show up in text-editor searches and make tech-debt visible.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
<#
respawn_gate — demonstrates TODO / FIXME comment conventions.
Place a trigger_device as the respawn zone boundary.
#>
respawn_gate := class(creative_device):
# TODO: replace with a player_spawner_device once the API is wired up
@editable BoundaryTrigger : trigger_device = trigger_device{}
# FIXME: cooldown is hardcoded — expose as @editable float when time allows
RespawnCooldownSeconds : float = 3.0 # seconds before player can respawn again
OnBegin<override>()<suspends> : void =
# NOTE: TriggeredEvent fires for ANY agent, including NPCs — filter if needed
BoundaryTrigger.TriggeredEvent.Subscribe(OnBoundaryEntered)
OnBoundaryEntered(Agent : ?agent) : void =
# Unwrap the optional agent; bail silently if no agent is present
if (A := Agent?):
# NOTE: actual respawn logic goes here once PlayerSpawner API is available
_ = A # placeholder — prevents unused-variable compile error
Pattern 3 — Block comment as a function contract
For complex helper methods, a multi-line block comment above the method acts as a contract: inputs, outputs, side-effects, and assumptions.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
<# elimination_bonus — awards bonus items on multi-kill streaks #>
elimination_bonus := class(creative_device):
@editable Granter : item_granter_device = item_granter_device{}
@editable EliminationTracker : tracker_device = tracker_device{}
OnBegin<override>()<suspends> : void =
# Poll the tracker once at start to establish baseline
_ = CheckStreakBonus()
<#
CheckStreakBonus
────────────────
Reads the current elimination count from EliminationTracker.
Returns true if the count qualifies for a streak bonus (>= 3),
false otherwise.
Side-effects: none — does NOT call Granter directly.
Caller is responsible for acting on the return value.
Assumptions:
- EliminationTracker is configured to track eliminations.
- GetStatValue() returns 0 if no eliminations have occurred yet.
#>
CheckStreakBonus() : logic =
Count : int = EliminationTracker.GetStatValue()
# A streak requires at least 3 eliminations
if (Count >= 3):
return true
return false
Gotchas
1. # comments are NOT block comments — they end at the newline
A common mistake from C/C++ backgrounds: # only comments out the rest of that line. If you write:
# This is line one
This is NOT a comment — it will cause a compile error
Use <# … #> any time you need a comment to span more than one line.
2. Inline block comments inside expressions must be balanced
Every <# must have a matching #>. An unclosed inline comment will consume everything after it to the end of the file, producing cryptic parse errors far from the actual mistake. Always close your block comments before saving.
3. Comments do not substitute for correct types
Verse is strongly typed. Writing # this is a float above an int variable does not change the type. If GetStatValue() returns int and you need a float, you must write Float := Float(StatTracker.GetStatValue()) — a comment cannot bridge the gap.
4. message parameters require <localizes>, not a comment workaround
Some device APIs accept a message type, not a plain string. You cannot pass a raw string literal or use a comment to "cast" it. Declare a localizer function:
MyText<localizes>(S : string) : message = "{S}"
Then pass MyText("Vault Opened!"). No amount of commenting will make a string satisfy a message parameter.
5. Commented-out code accumulates debt
It is tempting to comment out old logic with # instead of deleting it. In a version-controlled project (UEFN uses source control), deleted code is recoverable from history. Commented-out blocks confuse readers about what is actually active. Prefer deletion over commenting out, and use # TODO: markers for planned code.
6. Over-commenting is noise too
Avoid restating what the code already says clearly:
# BAD: set PlateAActive to true
set PlateAActive = true
# GOOD: latch stays true until both plates reset at round end
set PlateAActive = true
The best comment explains why, not what.