Verse Wiki — the Verse handbook for Blueprint authors
Chapter 7 · Lesson 25

Events and Binding: From Event Dispatcher to Subscribe

Blueprint authors decouple with Event Dispatchers: one thing shouts, and everything else decides for itself whether to listen. In Verse the same thing is called Subscribe. This lesson moves the broadcast/subscribe model out of the node graph and into code — plus how to reference another object, how to unsubscribe, and why events almost always beat Tick.

1. Broadcast and Subscribe: You Already Know This Model

Think back to the last time you used an Event Dispatcher in Blueprints. A door gets opened. The door has no idea who cares — it just Calls its own OnOpened dispatcher and shouts "I'm open." Whoever cares does the Binding: the scoreboard binds, the audio manager binds, the achievement system binds. The door never hears about any of them.

That is the broadcast / subscribe model, and it is the single most important decoupling tool in Blueprints. Its value isn't "it can trigger other logic" — holding a reference and calling a function triggers logic too. Its value is direction: the publisher doesn't know the subscribers, so you can add or remove a listener any time without going back to touch the door.

Verse expresses the same model with three words:

Verse also throws in something Blueprints don't have: Await() — instead of registering a handler, you park the current exec line right here until the event rings. There is no Blueprint node for this, because a Blueprint function graph cannot stop partway (Lesson 23). Verse has <suspends>, so "wait for an event" can sit on the exec line just like a Delay. We'll walk both routes below.

But before subscribing there's a more basic question to settle: how does your code get hold of that door in the first place?

2. Referencing Another Object: That Details-Panel Drag Is Called @editable

You could do the Blueprint version in your sleep: add a variable to the class, set its type to an Actor reference (or some concrete Actor class), tick the Instance Editable eye. Once the thing is in the level, open the Details panel, click the dropdown, and pick an Actor from the level. The code never knows which door it is — the door is configured in.

Verse works exactly the same way; the little eye is now one line of attribute: @editable.

remote_device.verse
using { /Fortnite.com/Devices }

# A remote control with slots
remote_device := class(creative_device):

    # Slot one: a button in the level. @editable goes on its own line, above the field
    @editable
    TriggerButton:button_device = button_device{}

    # Slot two: plain values can be exposed too - designers tweak numbers without touching code
    @editable
    Delay:float = 2.0

Line by line against Blueprints: the @editable line = ticking Instance Editable; TriggerButton:button_device = a "button-typed Actor reference variable"; = button_device{} = its default value. Delay:float = 2.0 is a Float in the variables panel defaulting to 2.0, which designers edit right in the Details panel.

Three rules to remember. First, @editable takes its own line, above the field. Second, a default value is mandatory — reference-typed fields use an empty archetype as placeholder (= button_device{}), and skipping it turns Compile red. A Blueprint object reference defaults to None; Verse refuses to have such an "empty" state at all, which is part of how it kills Accessed None at the root (see Lesson 18 on option). Third, more than object references can wear the attribute: int, float, logic, string and arrays of them all work; to put a whole class of your own on the panel, that class also needs the <concrete> marker (condition: every field has a default).

One thing is identical to Blueprints: changing a value on the panel needs no re-Compile. Nudging a cooldown from 2 s to 3 s is a drag, not a build. And that's exactly why "look the level object up by name in code" is a bad habit on both sides — levels change constantly, while a slot only needs a fresh drag.

3. Subscribing to an Event: Bind Event in Code Form

With a button in the slot, back to the opening question: how does the code find out the instant the button is pressed? In Blueprints you drop a Bind Event to On Interacted on the event graph and wire its red event pin to a Custom Event. In Verse it is one function call:

alarm_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

# Alarm: subscribing, unsubscribing, and plain waiting - all three moves at once
alarm_device := class(creative_device):

    @editable
    AlarmButton:button_device = button_device{}

    OnBegin<override>()<suspends>:void =
        # Subscribe: when the button is pressed, OnAlarm is called automatically
        Sub := AlarmButton.InteractedWithEvent.Subscribe(OnAlarm)
        # Subscribe hands back a cancelable; Sub.Cancel() unsubscribes

        # The other route: don't subscribe, just wait - Await is an async call
        FirstAgent := AlarmButton.InteractedWithEvent.Await()
        Print("Got the first press, OnBegin moves on")

    OnAlarm(Agent:agent):void =
        Print("Alarm! Someone pressed the button!")

Where does the subscription go? Same place as in Blueprints: wherever "runs once at the start" lives. In Blueprints that's Event BeginPlay; in Verse it's OnBegin. Elsewhere is legal but risky — see pitfall three below, the most common trap in this lesson.

The handler's signature has to match. In Blueprints a mismatched signature simply won't let you make the connection; in Verse a mismatch is a red Compile. InteractedWithEvent hands over "whoever pressed the button" as an agent (think the Instigator pin on a Blueprint event), so the handler must read OnAlarm(Agent:agent):void — one parameter short won't do, and typing it as player won't either. One more detail worth a tap on the chalkboard: inside Subscribe(OnAlarm) goes the function's name, with no parentheses and no arguments. You are handing the graph to the button for safekeeping so it can run later — not running it now. Wiring it up ≠ pulling the wire right now.

Unsubscribing. Subscribe hands back a cancelable (a cancel ticket); calling Cancel() on it is Blueprint's Unbind Event. Blueprints also give you Unbind All Events to clear the board in one node; Verse has no counterpart — you keep the tickets yourself, and when there are many, store them in var Subs:[]cancelable and Cancel() them one by one at cleanup. When you must unsubscribe and when you can let it ride gets a whole page in extra x1.

The handler cannot wait. The function you subscribe is the run-to-completion kind; its signature is fixed by the official API and carries no <suspends> — put a bare Sleep in there and it errors. This has the same flavor as Blueprint's rule that a Delay can't go inside a custom Function. The fix is to call in Lesson 24's clone technique: spawn{ SomeAsyncFunc() } forks a new wire and lets that wire do the sleeping and waiting. spawn happens to be the only concurrency expression allowed on a wire that can't wait; this is its home turf.

Assemble the parts and watch the whole chain: a slot + a counting variable + a subscription + a handler whose signature lines up = a doorbell that keeps books. Hit Run Next Step.

doorbell_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

# Counting doorbell: every press goes in the ledger
doorbell_device := class(creative_device):

    @editable
    Bell:button_device = button_device{}

    var PressCount:int = 0

    OnBegin<override>()<suspends>:void =
        Bell.InteractedWithEvent.Subscribe(OnRing)
        Print("Doorbell ready. Come press it.")

    OnRing(Agent:agent):void =
        set PressCount += 1
        Print("Ding-dong! Press number {PressCount}")
Output Log

Hit Run Next Step to watch the code execute line by line.

Notice what happens after OnBegin finishes: the device is not "over." It guards the wire it bound, and whenever a player presses, OnRing wakes up; ten presses, ten ledger entries — the very same mental model as BeginPlay finishing in Blueprints while the events you Bound keep working.

4. Firing Your Own Event: Call Dispatcher in Code Form

Native devices ship with events. But how do two Verse classes you wrote yourself talk? This is Event Dispatcher's home ground in Blueprints: Add Event Dispatcher on the class, name it, configure its inputs, then Call it wherever you need.

Verse's counterpart is event(t). Declaring one has an iron rule: the "type = archetype" pair must be written in full, i.e. RaceStartEvent<public>:event(agent) = event(agent){}. Write only the left half and Compile complains it is an abstract thing you cannot use directly. The agent in the parentheses is the dispatcher's input parameter type (the Inputs you configure on the Event Dispatcher panel); with no parameters, write event() = event(){}. The <public> is there so other classes can reach it — the other end of the conversation usually lives elsewhere.

starter_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# The Signal / Await of event(t) come from /Verse.org/Concurrency
# The Verse API Reference is the source of truth (using paths can differ across template versions)
using { /Verse.org/Concurrency }

# Starter side: one button press broadcasts the race start to every listener
starter_device := class(creative_device):

    @editable
    StartButton:button_device = button_device{}

    # The equivalent of Add Event Dispatcher; its input is one agent
    RaceStartEvent<public>:event(agent) = event(agent){}

    OnBegin<override>()<suspends>:void =
        StartButton.InteractedWithEvent.Subscribe(OnPressed)

    OnPressed(Agent:agent):void =
        # Signal is Call Dispatcher: shout, and take the Agent along
        RaceStartEvent.Signal(Agent)
        Print("The starting gun fires!")
gate_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

# Listener side: wait for the starting gun, then open the barrier
gate_device := class(creative_device):

    # @editable can reference a Verse class you wrote yourself
    @editable
    Starter:starter_device = starter_device{}

    @editable
    Gate:barrier_device = barrier_device{}

    OnBegin<override>()<suspends>:void =
        # Await is an async call; OnBegin has suspends, so this is legal
        Racer := Starter.RaceStartEvent.Await()
        Gate.Disable()
        Print("Starting gun fired - barrier open!")

Read the whole chain against Blueprints: the player presses the starter button → the OnPressed graph runs (in Blueprints, that Custom Event lights up) → RaceStartEvent.Signal(Agent) is Call Dispatcher, taking the player along → over on the other side, the exec line parked on Await wakes up holding AgentGate.Disable() opens the way. Note that the slot on gate_device references your own starter_device: in the editor, drag the starter in and the two are paired — the same gesture as dragging an Actor reference in Blueprints.

One semantic point deserves care: when several exec lines Await the same event at once, the wake-up order — and whether one awaiter or all of them wake — is described inconsistently across doc versions. When you need the deterministic "one broadcast, every listener hears it" behavior, register several handlers with Subscribe instead of having several wires each Await the same event. That also matches your Blueprint instinct: every Bind on an Event Dispatcher gets notified.

Your turn. The walkie-talkie below is missing its two key moves: one shouts (Call Dispatcher), one waits. Fill in the code words:

relay_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Concurrency }

# Walkie-talkie: press the button and the patrol gets the code word instantly
relay_device := class(creative_device):

    @editable
    GoButton:button_device = button_device{}

    GoEvent:event() = event(){}

    OnBegin<override>()<suspends>:void =
        GoButton.InteractedWithEvent.Subscribe(OnGo)
        WaitForGo()

    OnGo(Agent:agent):void =
        # Shout (the equivalent of Call Dispatcher)
        GoEvent.____()

    WaitForGo()<suspends>:void =
        # Park here until someone shouts
        GoEvent.____()
        Print("Code word received - move out!")

5. Why Events Beat Tick

One requirement — "open the door when the player presses the button" — two ways to write it. The polling version looks like this:

polling_vs_event.verse
# ✗ Polling (sketch): the equivalent of stuffing logic into Event Tick,
#   asking "pressed yet?" every frame. ButtonIsPressed is a logic field you
#   maintain yourself; this is here only to contrast the shapes.
PollForPress()<suspends>:void =
    loop:
        if (ButtonIsPressed?):
            OpenDoor()
        Sleep(0.0)

# ✓ Event-driven: register once, let the engine do the rest
OnBegin<override>()<suspends>:void =
    Bell.InteractedWithEvent.Subscribe(OnRing)

The polling version runs about 30 times a second (Lesson 23's simulation-frame rhythm), and in a typical match all 30 of those do the same thing: confirm "not pressed." The event version might run twice the whole match — because the player really did press twice. The gap isn't a few percent, it's orders of magnitude. And it scales with the number of objects: twenty doors means twenty exec lines spinning every frame.

Latency and correctness are the bigger problem, though. Polling notices a state change one frame late at best, and it only ever sees "what the state is now," never "what just happened" — a player who presses and releases within one frame slips straight through polling, while an event catches every single one. This is why Unreal performance write-ups have repeated the same sentence for decades: don't use Tick if you can avoid it. The advice carries into Verse word for word; Tick just goes by another name here — "a Sleep(0.0) in a loop body."

The engineering advice for a Blueprint author, in three lines:

6. Common Pitfalls: On the Wiring Floor

This lesson's pitfalls share one trait: half of them live outside the code. Walk the checklist and save yourself hours.

Blueprint Cross-Reference

Almost every concept in this lesson has a same-named thing in Blueprints. The differences column is the one that matters.

How you do it in Blueprints How you write it in Verse Difference
Add Event Dispatcher on the class, configure its Inputs MyEvent<public>:event(agent) = event(agent){} Verse demands the "type = archetype" pair in full; drop the right half and you get an abstract-type error. The type in the parentheses is the dispatcher's Inputs
A Bind Event to XXX node, red pin wired to a Custom Event Button.InteractedWithEvent.Subscribe(OnPressed) Blueprints validate the signature through the act of connecting (mismatch = no connection); Verse validates it in the compiler (mismatch = red Compile). You pass the function's name, no parentheses
Unbind Event / Unbind All Events Sub := ...Subscribe(...), then Sub.Cancel() Verse has no "unbind everything" button: you keep the tickets yourself, usually in var Subs:[]cancelable
The Call Dispatcher node MyEvent.Signal(Agent) Same meaning. But a Verse event can also be Awaited — a third usage Blueprints have no form for
An Actor reference variable (Object Reference type) An @editable field, e.g. Button:button_device = button_device{} A Blueprint object reference defaults to None; Verse forces a default value and refuses to have an "empty" state at all
Dragging a level Actor into that variable slot in the Details panel The same Details panel, dragged into the @editable slot Identical — Verse offers no code substitute for this step, and shouldn't: keeping configuration out of logic is the right call

The differences come from two things. One: Blueprints let the act of connecting a wire guarantee signature matching — if it doesn't fit, it doesn't connect. Verse has no wires, so that guarantee moves to the compiler, and you meet more red text, earlier. Two: Verse has <suspends>, so an event gains a way of being consumed beyond "register a callback" — parking an exec line on it. Blueprint's Bind has no such form, because a Blueprint exec line cannot stop.

And one place where Blueprints are genuinely handier, credit where it's due: the Event Dispatcher panel lets you open up "who bound to me." Verse has no such view — to learn how many subscribers an event has, you search the code. That is one of the prices text code pays for compile-time safety.

Level Challenges

Wiring practice complete — three challenges to cash it in. Zero penalty for wrong answers; retry as often as you like.

Compile all green, but the moment you press the button in game you get an "accessing invalid object" runtime error. The most likely cause?

You want to wait 3 seconds inside the handler you passed to Subscribe before opening the door. The right way is?

To write the equivalent of an Event Dispatcher in Verse, which spelling passes Compile?

Further Reading

Technique · EXTRA

The Lifecycle of a Subscription: When to Subscribe, When to Unsubscribe

Which line the subscription belongs on, how to keep the cancelable ticket, and why re-subscribing makes one press fire N times.

Open the extra →

Deep Dive · EXTRA

@editable References: Wiring Level Objects Into Code

From Actor reference variables to @editable fields: default values, array slots, referencing your own classes, and the renaming landmine.

Open the extra →

Advanced · EXTRA

Event-Driven vs Polling: Costing Out Two Architectures

The same requirement written both ways, with the bill itemized: per-second cost, response latency, missed events — and the few cases where polling still wins.

Open the extra →