Reference Verse

cancel-on-condition: Cancelling Async Tasks When the Game Says Stop

There is no single placed device called "cancel-on-condition" — it is a Verse PATTERN: you start an async task or event subscription, then cancel it the moment a game condition (a stat reached, a player referenced, a plate stepped on) becomes true. This article shows how to wield the `cancelable` result of `Subscribe()`, cooperative task cancellation at suspension points, and real device checks like `GetStatValue()` and `IsReferenced[]` to build responsive, self-cleaning logic.

Updated
The code on this reference page is provided as-is and did not pass the latest compile check — treat the examples as a starting point and verify in your project.
Watch the Knotcancel_on_condition in ~90 seconds.

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 @editable fields bind placed devices — a player_reference_device, a button_device, and a score_manager_device. You MUST declare devices as fields to call them.
  • var MaybeLoop : ?cancelable stores the handle to our running task so a different method can cancel it later. It starts false (no loop yet).
  • In OnBegin, StartButton.InteractedWithEvent.Subscribe(OnPressed) returns a cancelable we name ButtonSub.
  • The loop polls PlayerRef.GetStatValue() once per second. When the value reaches TargetStat, we unwrap MaybeLoop? and call Loop.Cancel() to stop the bonus task, then ButtonSub.Cancel() to kill the subscription, then break.
  • OnPressed arms the loop onceif (not MaybeLoop?) guards against double-arming — using spawn{ BonusLoop(Agent) }, which returns a cancelable.
  • BonusLoop awards score and Sleep(2.0)s. That Sleep is the suspension point: when Cancel() 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 a cancelable, not void. If you ignore the return value you can never stop the subscription. Store it in a field (often var Maybe... : ?cancelable) when a different method needs to cancel it.
  • Cancellation is cooperative. A spawn'd task only stops at a suspension point like Sleep or Await. A tight loop with no Sleep will never honor a Cancel() and will hang the game. Always give async loops a suspension point.
  • Unwrap options before use. MaybeLoop is ?cancelable; you must if (Loop := MaybeLoop?): before calling Loop.Cancel(). Same for the (Agent : ?agent) handed to listenable-agent events — unwrap with if (A := MaybeAgent?):.
  • IsReferenced[Agent] is failable (<decides>), not a bool. Call it inside an if (...) context with square brackets — never assign it to a logic.
  • 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 spawn with if (not MaybeLoop?) so pressing the button twice doesn't leak a second uncancellable task.

Build your own lesson with cancel_on_condition

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 →