Reference Verse compiles

canvas: Free-Positioned UI for a Sunny Cove HUD

The canvas widget is your blank beach towel for UI — you pin text, images, and buttons anywhere on the screen at exact coordinates. In this article we build a bright cel-shaded cove HUD that greets each player who lands on the shore, using canvas, canvas_slot, and the player_ui.

Updated Examples verified on the live UEFN compiler
Watch the Knotcanvas in ~90 seconds.

Overview

In UEFN, most UI is built from widgets — buttons, text blocks, images — but those widgets need a container that decides where they appear on screen. The canvas widget fills that role: it is a free-form layout panel where each child sits in its own canvas_slot that describes position, size, alignment, and draw order.

Use canvas when you need:

  • Overlapping elements (health bars, crosshairs, objective icons) at precise screen positions.
  • UI that changes layout at runtime (moving a marker, swapping a panel in/out).
  • Layered widgets with explicit z-ordering.

The two types you'll work with constantly are:

Type Kind Role
canvas class (widget) The container; call AddWidget / RemoveWidget at runtime
canvas_slot struct Describes one child's position, size, alignment, and z-order

canvas_slot fields you set when building a slot:

  • Anchors — a anchors struct with Minimum and Maximum vector2 values (0.0–1.0) that pin the slot relative to the canvas bounds.
  • Offsets — a margin struct (Left, Top, Right, Bottom) that shifts or sizes the slot in pixels.
  • Alignment — a vector2 pivot inside the widget itself (0.5, 0.5 = center).
  • ZOrderint draw order; higher = on top.
  • SizeToContentlogic; when true the widget sizes itself; when false Offsets.Right/Bottom set explicit width/height.
  • Widget — the child widget to display.

API Reference

(API surface could not be resolved for this device.)

Walkthrough

Scenario: A capture-the-flag map needs an on-screen "Objective" banner that slides in when the round starts, then disappears after 4 seconds. We build the banner as a text_block inside a canvas, add it to the player's HUD via player_ui, then remove it after the delay.

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

# Localized helper so we can pass a message to text_block
ObjectiveBannerText<localizes>(S : string) : message = "{S}"

objective_banner_device := class(creative_device):

    # Called once when the game session begins
    OnBegin<override>()<suspends> : void =
        # Iterate every player currently in the session
        Players := GetPlayspace().GetPlayers()
        for (Player : Players):
            ShowBannerForPlayer(Player)

    # Build and display the canvas banner for one player, then remove it
    ShowBannerForPlayer(Player : player)<suspends> : void =
        # Grab the player_ui component so we can push widgets onto their HUD
        if (PlayerUI := GetPlayerUI[Player]):
            # 1. Create the text widget that will be our banner
            BannerLabel := text_block:
                DefaultText := ObjectiveBannerText("⚑  Capture the Flag!")
                DefaultTextColor := MakeColorFromHex("FFDD00FF")

            # 2. Build a canvas_slot that anchors the banner to the top-center
            #    Anchors.Minimum/Maximum both at (0.5, 0.0) = top-center point anchor
            #    Offsets.Left = -300 shifts left by half the widget width (600 px wide)
            #    Offsets.Top  = 80  drops it 80 px from the top edge
            #    SizeToContent = false so Right/Bottom set explicit 600×80 px size
            BannerSlot := canvas_slot:
                Anchors := anchors:
                    Minimum := vector2{X := 0.5, Y := 0.0}
                    Maximum := vector2{X := 0.5, Y := 0.0}
                Offsets := margin:
                    Left   := -300.0
                    Top    := 80.0
                    Right  := 600.0
                    Bottom := 80.0
                Alignment    := vector2{X := 0.0, Y := 0.0}
                ZOrder       := 10
                SizeToContent := false
                Widget       := BannerLabel

            # 3. Create the canvas and add the slot during construction via Slots
            BannerCanvas := canvas:
                Slots := array{BannerSlot}

            # 4. Push the canvas onto the player's HUD
            PlayerUI.AddWidget(BannerCanvas)

            # 5. Wait 4 seconds, then tear it down
            Sleep(4.0)
            PlayerUI.RemoveWidget(BannerCanvas)```

**Line-by-line breakdown:**

- **`GetPlayspace().GetPlayers()`**  fetches every player so we show the banner to everyone.
- **`text_block`**  a built-in widget; `DefaultText` requires a `message`, so we use the `<localizes>` helper.
- **`canvas_slot`**  a struct literal; every field is set inline. `Anchors.Minimum == Anchors.Maximum` creates a *point anchor* (no stretching).
- **`Offsets`**  because `SizeToContent` is `false`, `Right` and `Bottom` are the widget's **width** and **height** in pixels, not a right/bottom margin.
- **`canvas{ Slots := array{BannerSlot} }`**  the `Slots` field is only read at construction time; use `AddWidget` / `RemoveWidget` for runtime changes.
- **`PlayerUI.AddWidget(BannerCanvas)`**  pushes the whole canvas onto the HUD.
- **`Sleep(4.0)`**  suspends this coroutine for 4 seconds (the function is `<suspends>`).
- **`PlayerUI.RemoveWidget(BannerCanvas)`**  cleanly removes the canvas after the delay.

## Common patterns

### Pattern 1 — Dynamically swap a widget inside a running canvas

Swap an icon widget out for a different one at runtime using `RemoveWidget` then `AddWidget`.

```verse
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /Fortnite.com/Characters }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }

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

icon_swap_device := class(creative_device):

    OnBegin<override>()<suspends> : void =
        Players := GetPlayspace().GetPlayers()
        if (Player := Players[0], PlayerUI := GetPlayerUI[Player]):
            # Build first icon
            IconA := text_block:
                DefaultText := SwapIconText("🔴 Red Team Leading")

            SlotA := canvas_slot:
                Anchors := anchors:
                    Minimum := vector2{X := 0.0, Y := 1.0}
                    Maximum := vector2{X := 0.0, Y := 1.0}
                Offsets := margin:
                    Left   := 40.0
                    Top    := -60.0
                    Right  := 400.0
                    Bottom := 50.0
                Alignment     := vector2{X := 0.0, Y := 1.0}
                ZOrder        := 5
                SizeToContent := false
                Widget        := IconA

            HudCanvas := canvas:
                Slots := array{SlotA}

            PlayerUI.AddWidget(HudCanvas)

            # Simulate a score change after 5 seconds
            Sleep(5.0)

            # Remove old icon, add new one
            HudCanvas.RemoveWidget(IconA)

            IconB := text_block:
                DefaultText := SwapIconText("🔵 Blue Team Leading")

            SlotB := canvas_slot:
                Anchors := anchors:
                    Minimum := vector2{X := 0.0, Y := 1.0}
                    Maximum := vector2{X := 0.0, Y := 1.0}
                Offsets := margin:
                    Left   := 40.0
                    Top    := -60.0
                    Right  := 400.0
                    Bottom := 50.0
                Alignment     := vector2{X := 0.0, Y := 1.0}
                ZOrder        := 5
                SizeToContent := false
                Widget        := IconB

            HudCanvas.AddWidget(SlotB)

Pattern 2 — Layered canvas with multiple slots at different z-orders

Stack a background panel behind a foreground label using ZOrder.

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

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

layered_hud_device := class(creative_device):

    OnBegin<override>()<suspends> : void =
        Players := GetPlayspace().GetPlayers()
        if (Player := Players[0], PlayerUI := GetPlayerUI[Player]):

            # Background: a colored overlay widget at ZOrder 0
            Background := color_block:
                DefaultColor := MakeColorFromHex("000000AA")  # semi-transparent black

            BackSlot := canvas_slot:
                Anchors := anchors:
                    Minimum := vector2{X := 0.5, Y := 0.5}
                    Maximum := vector2{X := 0.5, Y := 0.5}
                Offsets := margin:
                    Left   := -200.0
                    Top    := -40.0
                    Right  := 400.0
                    Bottom := 80.0
                Alignment     := vector2{X := 0.0, Y := 0.0}
                ZOrder        := 0
                SizeToContent := false
                Widget        := Background

            # Foreground: text on top at ZOrder 1
            Label := text_block:
                DefaultText := LayerLabelText("Zone Closing in 10s")

            FrontSlot := canvas_slot:
                Anchors := anchors:
                    Minimum := vector2{X := 0.5, Y := 0.5}
                    Maximum := vector2{X := 0.5, Y := 0.5}
                Offsets := margin:
                    Left   := -180.0
                    Top    := -20.0
                    Right  := 360.0
                    Bottom := 40.0
                Alignment     := vector2{X := 0.0, Y := 0.0}
                ZOrder        := 1
                SizeToContent := false
                Widget        := Label

            # Both slots initialized via Slots array at construction
            LayeredCanvas := canvas:
                Slots := array{BackSlot, FrontSlot}

            PlayerUI.AddWidget(LayeredCanvas)
            Sleep(10.0)
            PlayerUI.RemoveWidget(LayeredCanvas)

Pattern 3 — SizeToContent: let the widget determine its own size

When you don't know the pixel dimensions ahead of time (e.g. dynamic text), set SizeToContent := true and use Offsets.Left/Top only for position.

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

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

auto_size_hud_device := class(creative_device):

    OnBegin<override>()<suspends> : void =
        Players := GetPlayspace().GetPlayers()
        if (Player := Players[0], PlayerUI := GetPlayerUI[Player]):

            DynamicLabel := text_block:
                DefaultText := AutoSizeText("Kills: 0")

            # SizeToContent = true: Right/Bottom are IGNORED for sizing.
            # Only Left/Top in Offsets matter — they position the top-left corner.
            AutoSlot := canvas_slot:
                Anchors := anchors:
                    Minimum := vector2{X := 1.0, Y := 0.0}
                    Maximum := vector2{X := 1.0, Y := 0.0}
                Offsets := margin:
                    Left   := -220.0
                    Top    := 30.0
                    Right  := 0.0
                    Bottom := 0.0
                Alignment     := vector2{X := 1.0, Y := 0.0}
                ZOrder        := 3
                SizeToContent := true
                Widget        := DynamicLabel

            KillCanvas := canvas:
                Slots := array{AutoSlot}

            PlayerUI.AddWidget(KillCanvas)
            # Keep alive for the session
            Sleep(Inf)

Gotchas

1. Slots is construction-only — use AddWidget/RemoveWidget at runtime

The Slots field on canvas is only read when the canvas is first created. If you try to mutate Slots directly after construction nothing will happen. Always call canvas.AddWidget(slot) or canvas.RemoveWidget(widget) to change children at runtime.

2. SizeToContent flips the meaning of Offsets.Right and Offsets.Bottom

  • SizeToContent := falseRight = width, Bottom = height in pixels.
  • SizeToContent := trueRight and Bottom are ignored; the widget measures itself. Mixing these up is the #1 source of invisible or zero-size widgets.

3. Point anchors vs. stretch anchors

  • Point anchor: Minimum == Maximum (e.g. both vector2{X:=0.5, Y:=0.5}). The slot is positioned relative to that point; Offsets.Left/Top shift it.
  • Stretch anchor: Minimum != Maximum (e.g. Min {0,0} Max {1,1}). The slot stretches to fill the region; Offsets become inset margins from each edge. Accidentally using stretch anchors when you want a fixed-size widget will make it fill the screen.

4. message is not a string

text_block.DefaultText requires a message, not a string. Always declare a <localizes> helper:

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

There is no StringToMessage function.

5. GetPlayerUI is failable — always guard with if

GetPlayerUI[Player] uses the subscript (failable) form. Wrap it in if (PlayerUI := GetPlayerUI[Player]): or the compiler will reject it.

6. Sleep requires a <suspends> context

Any function that calls Sleep must be marked <suspends>. If you call ShowBannerForPlayer from OnBegin, that's fine because OnBegin is already <suspends>. But if you call it from a synchronous event handler you'll need to spawn it: spawn{ ShowBannerForPlayer(Player) }.

Guides & scripts that use canvas

Step-by-step tutorials that put this object to work.

Build your own lesson with canvas

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 →