Runtime Reticle UI Driven by a Tick Loop
Tutorial intermediate

Runtime Reticle UI Driven by a Tick Loop

Updated intermediate

What you'll learn

  • Resolving players and their per-player UI through GetPlayspace() and GetPlayerUI[].
  • Building a canvas overlay and adding a text_block reticle with AddWidget.
  • Subscribing to a per-frame update using the real TickEvent.Subscribe signature (a callback taking a float delta).
  • Driving the widget's runtime state safely from a <transacts> handler and cleaning up the subscription with cancelable.Cancel().

How it works

The device runs OnBegin when the level starts. It walks every player from GetPlayspace().GetPlayers(), and for each one it fetches that player's UI surface with GetPlayerUI[Player] — note the square brackets, because GetPlayerUI is a fallible (<decides>) lookup and MUST live inside an if.

Once we have the UI, we build a canvas and place a text_block inside a canvas_slot, then push the whole canvas onto the player's screen with AddWidget. The text_block is our reticle; we keep a reference so we can mutate it later.

For the live behavior we subscribe to the playspace TickEvent. Its real signature hands our callback a single float (the delta time between frames), and Subscribe returns a cancelable — we store it so we can Cancel() the loop when we're done. Each frame we accumulate elapsed time and rewrite the reticle text through SetText. Because updating widget state is a mutation, the handler is marked <transacts>, matching the effect requirements of the APIs it calls.

Let's build it

Create a Verse file named runtime_reticle.verse, paste the code below, then build (Ctrl+Shift+B) and drop the device into your level.

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

# Device that attaches a live, per-frame reticle to each player's screen.
runtime_reticle := class<concrete>(creative_device):

    # How long the encounter has been running, driven by the tick delta.
    var ElapsedTime : float = 0.0

    # The reticle text widget we mutate every frame.
    var Reticle : text_block = text_block{}

    # Stored so we can stop the per-frame loop later.
    var TickHandle : ?cancelable = false

    OnBegin<override>()<suspends>: void =
        # Build the reticle UI for every player in the playspace.
        for (Player : GetPlayspace().GetPlayers()):
            # GetPlayerUI is fallible — it must be resolved inside an if.
            if (PlayerUI := GetPlayerUI[Player]):
                # Fresh text_block acts as our reticle glyph.
                set Reticle = text_block{DefaultText := "( + )"}
                # Canvas lets us position the widget on-screen.
                ReticleCanvas := canvas:
                    Slots := array:
                        canvas_slot:
                            Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
                            Alignment := vector2{X := 0.5, Y := 0.5}
                            Widget := Reticle
                # Push the canvas onto this player's screen.
                PlayerUI.AddWidget(ReticleCanvas)

        # Subscribe to the per-frame TickEvent. Its callback takes the
        # frame delta (float); Subscribe returns a cancelable we keep.
        set TickHandle = option{GetPlayspace().GetTickEvent().Subscribe(UpdateReticle)}

    # Runs once per frame. <transacts> because it mutates widget state.
    UpdateReticle(DeltaTime : float)<transacts>: void =
        # Accumulate elapsed time from the per-frame delta.
        set ElapsedTime = ElapsedTime + DeltaTime
        # Rewrite the reticle text with live, interpolated data.
        Reticle.SetText(StringToMessage("( + )  {Round[ElapsedTime]}s"))

    # Helper: turn a string into the message SetText expects.
    StringToMessage<localizes>(S : string)<computes> : message = "{S}"

GetPlayerUI[Player] uses [] because it is a <decides> lookup; calling it with () would not compile. The TickEvent.Subscribe callback signature is _(:float):void, so UpdateReticle takes exactly one float. We stash the returned cancelable in a ?cancelable so it can be cancelled later without crashing if the loop never started.

Try it yourself

  • Add a @editable float for a pulse speed and use Sin of ElapsedTime to make the reticle text flash between two glyphs.
  • Cancel the loop after a set duration: bind the option (if (H := TickHandle?): H.Cancel()) once ElapsedTime passes a threshold.
  • Swap the text_block for a colored one and change its text based on how long the encounter has run.
  • Store one reticle per player in a [player]text_block map instead of a single shared field so each screen is independent.

Recap

You attached a real widget to a player's screen with GetPlayerUI[] + AddWidget, drove it every frame through the genuine TickEvent.Subscribe API (whose callback receives a float delta), mutated widget state inside a <transacts> handler, and kept the returned cancelable so the loop can be cleanly stopped. These are the load-bearing pieces of any live, code-driven HUD in UEFN.

Check your understanding

Test yourself with an interactive quiz and track your progress + earn XP — free for members.

Turn this into a guided course

Add Implementing Camera Soft-Lock and Dynamic Target UI via Verse to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.

Original tutorial generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.

Comments

    Sign in to vote, comment, or suggest an edit. Sign in