Canvas Layout: The Rally HUD Panel

In this lesson you'll learn to

  • βœ“Compose a canvas with anchored slots for lap, boost meter, and position.

Compose a canvas with anchored slots for lap, boost meter, and position.

πŸ“– Reference & full walkthrough: Canvas Layout: The Rally HUD Panel

πŸ” Builds on: lesson 31 widget code (extends the same ui module)

🧩 Your capstone piece: Full rally HUD: laps + boosts + race position in one canvas


Overview

A canvas is a container widget that lets you position child widgets at arbitrary coordinates on the screen. Where a stack or grid decides layout for you, a canvas hands you full 2D control: you place each child in a canvas_slot, give it an anchor and an offset, and it draws exactly there.

Reach for a canvas whenever your on-screen art needs precise placement β€” a banner pinned top-center, a coin counter tucked in a corner, a Welcome to the Cove! splash floating over the lagoon. The canvas itself never fires events and has no methods you call directly (it's just the container). The real work is: build the canvas, drop widgets into its slots, then hand the whole thing to a player's player_ui with AddWidget. To take it back down, you call RemoveWidget.

The key players in this workflow:

  • canvas β€” the container you fill with slots.
  • GetPlayerUI(Player) β€” grabs the per-player UI layer (it can fail, so it's a <decides> call).
  • player_ui.AddWidget / RemoveWidget β€” show and hide your canvas on one player's screen.

API Reference

canvas

Canvas is a container widget that allows for arbitrary positioning of widgets in the canvas' slots.

Full public surface, resolved verbatim from the live Epic digest (UnrealEngine.digest.verse). Inherited members are merged from widget.

canvas<native><public> := class<final>(widget):

player

Full public surface, resolved verbatim from the live Epic digest (Verse.digest.verse). Inherited members are merged from agent.

player<native><public> := class<unique><persistent><module_scoped_var_weak_map_key><epic_internal>(agent):

vector3

3-dimensional vector with float components.

Full public surface, resolved verbatim from the live Epic digest (Verse.digest.verse).

vector3<native><public> := struct<concrete><computes><persistable><uht_comparable>:

Walkthrough

Here's the sunny scenario: a trigger_device sits on the wooden dock at the edge of a cove. When a player steps onto it, we build a canvas holding a welcome banner and a small hint, and pin it to that player's screen. Step off (a second trigger) and we tear it back down.

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

cove_hud_device := class(creative_device):

    # The dock plate the player steps on.
    @editable
    ArrivalPlate : trigger_device = trigger_device{}

    # A second plate at the far end that clears the HUD.
    @editable
    DeparturePlate : trigger_device = trigger_device{}

    # Localized text helper β€” message params never take raw strings.
    WelcomeText<localizes>(S : string) : message = "{S}"

    # Remember which canvas we gave each player, so we can remove it later.
    var ShownCanvases : [player]canvas = map{}

    OnBegin<override>()<suspends> : void =
        ArrivalPlate.TriggeredEvent.Subscribe(OnArrived)
        DeparturePlate.TriggeredEvent.Subscribe(OnDeparted)

    # Builds a filled canvas for one player.
    BuildCoveCanvas() : canvas =
        Banner := text_block:
            DefaultText := WelcomeText("Welcome to the Cove!")
            DefaultTextColor := color{ R := 1.0, G := 0.9, B := 0.4 }
        Hint := text_block:
            DefaultText := WelcomeText("Grab a board and hit the waves")
            DefaultTextColor := color{ R := 1.0, G := 1.0, B := 1.0 }
        canvas:
            Slots := array:
                canvas_slot:
                    Anchors := anchors{ Minimum := vector2{ X := 0.5, Y := 0.15 }, Maximum := vector2{ X := 0.5, Y := 0.15 } }
                    Offsets := margin{ Left := -200.0, Top := 0.0, Right := 200.0, Bottom := 60.0 }
                    Alignment := vector2{ X := 0.5, Y := 0.5 }
                    Widget := Banner
                canvas_slot:
                    Anchors := anchors{ Minimum := vector2{ X := 0.5, Y := 0.25 }, Maximum := vector2{ X := 0.5, Y := 0.25 } }
                    Offsets := margin{ Left := -200.0, Top := 0.0, Right := 200.0, Bottom := 40.0 }
                    Alignment := vector2{ X := 0.5, Y := 0.5 }
                    Widget := Hint

    OnArrived(Agent : ?agent) : void =
        if (A := Agent?, Player := player[A], UI := GetPlayerUI[Player]):
            NewCanvas := BuildCoveCanvas()
            UI.AddWidget(NewCanvas)
            if (set ShownCanvases[Player] = NewCanvas) {}

    OnDeparted(Agent : ?agent) : void =
        if (A := Agent?, Player := player[A], UI := GetPlayerUI[Player]):
            if (Old := ShownCanvases[Player]):
                UI.RemoveWidget(Old)

Line by line:

  • @editable ArrivalPlate / DeparturePlate β€” two trigger_devices you wire up in the editor by clicking the fields on this device. Without the @editable field a placed device can't be called from Verse.
  • WelcomeText<localizes> β€” a helper that turns a plain string into a message. UI text params require a localized value; there is no StringToMessage.
  • var ShownCanvases : [player]canvas β€” a map so each player's canvas is remembered independently. That's how RemoveWidget later knows which widget to pull.
  • OnBegin subscribes both plates' TriggeredEvent to handler methods. Handlers are methods on the class, subscribed here.
  • BuildCoveCanvas constructs two text_blocks, then a canvas whose Slots array holds two canvas_slots. Each slot's Anchors place it as a fraction of the screen (0.5 = horizontal center), Offsets give a pixel-sized box, and Alignment centers the widget on its anchor point.
  • OnArrived unwraps the optional agent with Agent?, casts to player, and grabs GetPlayerUI[Player] inside the same if β€” all three must succeed. Then AddWidget draws the canvas on that one player's screen, and we store it in the map.
  • OnDeparted looks up the stored canvas and calls RemoveWidget to clear it.

Common patterns

Pattern 1 β€” Pin a single label to a screen corner. A minimal canvas with one slot anchored to the top-left, perfect for a lagoon coin counter.

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

corner_label_device := class(creative_device):

    @editable
    ShowButton : button_device = button_device{}

    LabelText<localizes>(S : string) : message = "{S}"

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

    OnPressed(Agent : agent) : void =
        if (Player := player[Agent], UI := GetPlayerUI[Player]):
            Coins := text_block:
                DefaultText := LabelText("Shells: 0")
                DefaultTextColor := color{ R := 0.2, G := 0.8, B := 1.0 }
            Corner := canvas:
                Slots := array:
                    canvas_slot:
                        Anchors := anchors{ Minimum := vector2{ X := 0.0, Y := 0.0 }, Maximum := vector2{ X := 0.0, Y := 0.0 } }
                        Offsets := margin{ Left := 40.0, Top := 40.0, Right := 220.0, Bottom := 80.0 }
                        Widget := Coins
            UI.AddWidget(Corner)

Pattern 2 β€” Hide the HUD without removing it. Instead of RemoveWidget, keep the canvas mounted and toggle SetVisibility β€” cheaper when you'll show it again soon.

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

toggle_hud_device := class(creative_device):

    @editable
    ToggleButton : button_device = button_device{}

    TipText<localizes>(S : string) : message = "{S}"

    var MaybeCanvas : ?canvas = false

    OnBegin<override>()<suspends> : void =
        ToggleButton.InteractedWithEvent.Subscribe(OnToggle)

    OnToggle(Agent : agent) : void =
        if (Player := player[Agent], UI := GetPlayerUI[Player]):
            if (Existing := MaybeCanvas?):
                # Already built β€” flip visibility.
                Existing.SetVisibility(widget_visibility.Hidden)
            else:
                Tip := text_block:
                    DefaultText := TipText("Watch the tide!")
                    DefaultTextColor := color{ R := 1.0, G := 1.0, B := 1.0 }
                Built := canvas:
                    Slots := array:
                        canvas_slot:
                            Anchors := anchors{ Minimum := vector2{ X := 0.5, Y := 0.85 }, Maximum := vector2{ X := 0.5, Y := 0.85 } }
                            Offsets := margin{ Left := -150.0, Top := 0.0, Right := 150.0, Bottom := 50.0 }
                            Alignment := vector2{ X := 0.5, Y := 0.5 }
                            Widget := Tip
                UI.AddWidget(Built)
                set MaybeCanvas = option{ Built }

Pattern 3 β€” Clear a placed device's state alongside the HUD. Many gameplay devices expose Clear() to reset. Here a trigger both wipes a hud_message_device's state and pulls the canvas.

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

reset_zone_device := class(creative_device):

    @editable
    ResetPlate : trigger_device = trigger_device{}

    @editable
    HudMessage : hud_message_device = hud_message_device{}

    var Shown : [player]canvas = map{}

    OnBegin<override>()<suspends> : void =
        ResetPlate.TriggeredEvent.Subscribe(OnReset)

    OnReset(Agent : ?agent) : void =
        if (A := Agent?, Player := player[A], UI := GetPlayerUI[Player]):
            HudMessage.Clear()
            if (Old := Shown[Player]):
                UI.RemoveWidget(Old)

Gotchas

  • GetPlayerUI can fail. It's a <decides> function, so you must call it inside an if (... = GetPlayerUI[Player]) (square brackets) or a for/if conditional block. Calling it like a plain function won't compile.
  • UI is per-player. AddWidget adds to one player's screen. There is no global screen β€” loop over the players you care about and add a fresh canvas to each. Sharing one widget instance across players is not supported; build one per player.
  • Track what you added. RemoveWidget needs the exact widget reference you passed to AddWidget. Store it (a [player]canvas map or a ?canvas var) at add-time or you'll have no handle to remove.
  • Message params, not strings. DefaultText wants a message. Use a <localizes> helper like WelcomeText(S:string):message; passing a bare "..." string fails to typecheck.
  • Anchors vs. Offsets. Anchors are screen fractions (0.0–1.0); Offsets are pixels relative to those anchors. If your widget vanishes off-screen, check that your anchor fraction and pixel offset agree β€” a 0.5 center anchor still needs a negative Left offset to appear centered.
  • The canvas has no events. Don't look for a canvas.OnClicked β€” the canvas is only a container. Interaction comes from the child widgets (like button_widget) or from placed devices (trigger_device, button_device) that you subscribe to.
πŸ”’

Keep going β€” free

You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.

Sign in free to continue this lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/canvas-layout
Source

Biloxi Studios original lesson

Guided course

Add this lesson to your free study plan.

🧭 The Keeper’s log

Quest complete? Chart your next heading from the 🐻 North Jungle expedition.

β›΅ Take me there