Overview
Every Subscribe() call in Verse returns a cancelable. Every spawn{}'d async task can be cancelled. "Cancel-on-condition" is the discipline of holding onto those handles and calling Cancel() — or racing a task against a stop signal — so your logic stops the instant the game state you care about flips.
The game problems this solves:
- A bonus-score loop that keeps ticking score up until a player reaches a target, tracked by a Player Reference device's
GetStatValue(). - A one-shot vault button where the first press cancels its own subscription so it can never fire twice.
- A guard-focus task on an AI that should stop looking at a player the moment that player is no longer the referenced target (
IsReferenced[]).
Because Verse cancellation is cooperative, tasks only actually stop at suspension points (like Sleep). That is the key mental model: you don't kill a task, you signal it and let it unwind cleanly. Reach for this pattern whenever an async behavior must not outlive the condition that justified it.
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: a bonus-score arena. When a match starts we spawn a loop that keeps awarding points to a tracked player via a score manager. A Player Reference device watches that same player's stat with GetStatValue(). The moment their stat crosses a target, we Cancel() the bonus loop so it stops ticking — and we Cancel() the button subscription too so the whole thing is one-and-done.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# A bonus-score loop that cancels itself when a tracked stat crosses a target.
cancel_on_condition_device := class(creative_device):
# The Player Reference device exposes GetStatValue() and IsReferenced[].
@editable
PlayerRef : player_reference_device = player_reference_device{}
# Button that arms the bonus loop.
@editable
StartButton : button_device = button_device{}
# Score awarded each tick.
@editable
ScoreManager : score_manager_device = score_manager_device{}
# Stat value that ends the bonus.
@editable
TargetStat : int = 100
# Held so we can cancel it later.
var MaybeLoop : ?cancelable = false
OnBegin<override>()<suspends>:void =
# Subscribe returns a cancelable; store it so a later press can be ignored.
ButtonSub := StartButton.InteractedWithEvent.Subscribe(OnPressed)
# Watch the tracked stat forever; when it crosses, cancel everything.
loop:
Sleep(1.0)
Current := PlayerRef.GetStatValue()
if (Current >= TargetStat):
# Condition met: cancel the running bonus loop.
if (Loop := MaybeLoop?):
Loop.Cancel()
Print("Bonus loop cancelled — target reached.")
# Also cancel the button so it can't re-arm.
ButtonSub.Cancel()
break
OnPressed(Agent : agent):void =
# Only arm once: if a loop already exists, ignore.
if (not MaybeLoop?):
NewLoop := spawn{ BonusLoop(Agent) }
set MaybeLoop = option{ NewLoop }
# The cancelable async task. Sleep is the suspension point where
# cancellation is actually honored.
BonusLoop(Agent : agent)<suspends>:void =
loop:
ScoreManager.Activate(Agent)
Sleep(2.0)
Line by line:
- The
@editablefields bind placed devices — aplayer_reference_device, abutton_device, and ascore_manager_device. You MUST declare devices as fields to call them. var MaybeLoop : ?cancelablestores the handle to our running task so a different method can cancel it later. It startsfalse(no loop yet).- In
OnBegin,StartButton.InteractedWithEvent.Subscribe(OnPressed)returns acancelablewe nameButtonSub. - The
looppollsPlayerRef.GetStatValue()once per second. When the value reachesTargetStat, we unwrapMaybeLoop?and callLoop.Cancel()to stop the bonus task, thenButtonSub.Cancel()to kill the subscription, thenbreak. OnPressedarms the loop once —if (not MaybeLoop?)guards against double-arming — usingspawn{ BonusLoop(Agent) }, which returns acancelable.BonusLoopawards score andSleep(2.0)s. ThatSleepis the suspension point: whenCancel()fires, the task unwinds cleanly here rather than mid-award.
Common patterns
One-shot subscription — cancel yourself on first fire. A vault button that can only ever be pressed once. Store the cancelable, then cancel it from inside the handler.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
one_shot_vault_device := class(creative_device):
@editable
VaultButton : button_device = button_device{}
@editable
VaultDoor : door_device = door_device{}
var MaybeSub : ?cancelable = false
OnBegin<override>()<suspends>:void =
Sub := VaultButton.InteractedWithEvent.Subscribe(OnVaultPressed)
set MaybeSub = option{ Sub }
OnVaultPressed(Agent : agent):void =
VaultDoor.Open()
# Cancel our own subscription so the button never fires again.
if (Sub := MaybeSub?):
Sub.Cancel()
Print("Vault opened — button disarmed.")
Race a task against a stop signal. Use race{} so a guard's focus behavior lives only until a plate is stepped on. When either branch finishes, the other is cancelled.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
focus_until_stop_device := class(creative_device):
@editable
StopPlate : trigger_device = trigger_device{}
@editable
PatrolManager : score_manager_device = score_manager_device{}
OnBegin<override>()<suspends>:void =
# Race the endless patrol loop against the stop plate.
# Whichever finishes first cancels the other.
race:
PatrolLoop()
WaitForStop()
Print("Patrol ended by stop condition.")
PatrolLoop()<suspends>:void =
loop:
Sleep(3.0) # suspension point — cancellable
WaitForStop()<suspends>:void =
# Completes the instant the plate is triggered, cancelling PatrolLoop.
StopPlate.TriggeredEvent.Await()
Cancel based on a Player Reference check. Only keep a reward loop alive while the tracked agent is still the referenced player (IsReferenced[] is a failable check — use it in an if).
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
referenced_reward_device := class(creative_device):
@editable
PlayerRef : player_reference_device = player_reference_device{}
@editable
RewardTrigger : trigger_device = trigger_device{}
OnBegin<override>()<suspends>:void =
RewardTrigger.TriggeredEvent.Subscribe(OnRewardTriggered)
OnRewardTriggered(MaybeAgent : ?agent):void =
if (Agent := MaybeAgent?):
# Only reward while this agent is the device's referenced player.
if (PlayerRef.IsReferenced[Agent]):
Print("Referenced player rewarded.")
else:
Print("Not the referenced player — skipped.")
Gotchas
Subscribe()returns acancelable, notvoid. If you ignore the return value you can never stop the subscription. Store it in a field (oftenvar Maybe... : ?cancelable) when a different method needs to cancel it.- Cancellation is cooperative. A
spawn'd task only stops at a suspension point likeSleeporAwait. A tightloopwith noSleepwill never honor aCancel()and will hang the game. Always give async loops a suspension point. - Unwrap options before use.
MaybeLoopis?cancelable; you mustif (Loop := MaybeLoop?):before callingLoop.Cancel(). Same for the(Agent : ?agent)handed to listenable-agent events — unwrap withif (A := MaybeAgent?):. IsReferenced[Agent]is failable (<decides>), not a bool. Call it inside anif (...)context with square brackets — never assign it to alogic.- Transactional cancel semantics. If you call
Cancel()inside a transaction that later rolls back, the subscription stays active. Cancel from a committed context. - Don't double-arm. Guard
spawnwithif (not MaybeLoop?)so pressing the button twice doesn't leak a second uncancellable task.