DEMIENGINEDOCUMENTATION Everything is free
UI & HUD · Create

Setters, events, tweens, and runtime-created nodes.

HUD from Lua

Layout is authored in JSON; scripts can change node content, state, and rectangles or create nodes while the game runs. This page covers addressing nodes, setters, events, tweens, and runtime nodes.

find and handles

Hud.find returns a handle for structural operations such as clone, remove, reparent, tween, and row recycling. Content and state setters take node ID strings.

local Hud = require("demi.hud")

local node = Hud.find("health_bar")   -- handle, or nil if missing
if node then
    Hud.set_value(node.id, 1.0)
end
  • Hud.find(id) looks up an authored or runtime node by id. Returns a generation-checked handle, or nil when no such node exists.
  • A stale handle is a handle kept after the node was removed. Generation checking makes structural operations fail instead of touching a reused slot. Re-find after remove / clear_children.
  • Setters look up the current node by ID. Keep handles for operations that require them.

Setters

Each node setter takes an ID string first and returns a success boolean.

CallEffectReturns
Hud.set_text(node, s)Sets label/text/text_input content.boolean
Hud.get_text(node)Reads current text.string, or nil for an unknown node
Hud.set_font(node, font)Switches the Font2D asset (asset://…); an empty string restores automatic fallback.boolean
Hud.set_font_size(node, n)Sets text height in canvas units.boolean
Hud.set_color(node, r, g, b, a?)Sets text/foreground color (0…1 components).boolean
Hud.set_background_color(node, r, g, b, a?)Sets the node’s background color.boolean
Hud.set_value(node, n)Sets progress/slider value within its range.boolean
Hud.set_checked(node, b)Sets toggle state.boolean
Hud.set_disabled(node, b)Enables or blocks input on the control.boolean
Hud.set_visible(node, b)Shows or hides the node.boolean
Hud.set_opacity(node, n)Sets alpha 0…1.boolean
Hud.set_image(node, texture, source_x, source_y, source_w, source_h)Swaps an image’s texture and source rectangle.boolean
Hud.set_position(node, x, y)Moves the node in canvas units.boolean
Hud.set_size(node, w, h)Resizes the node in canvas units.boolean
Hud.focus_next(reverse?)Moves focus to the next control (previous when reverse is true).boolean
Hud.focused()Which node has focus.focused node ID, or an empty string
local Hud = require("demi.hud")

Hud.set_text("score", "Score: 1200")
print(Hud.get_text("seed_input"))     -- e.g. "sky-01"
Hud.set_value("health_bar", 0.75)
Hud.set_disabled("start", false)
Hud.set_visible("hint", false)
Hud.set_opacity("fade", 0.5)
print(Hud.focused())                  -- "start" or ""

Node setters and Hud.get_text take an ID string. Hud.focus_next returns a boolean; Hud.focused returns an ID string (empty when no node is focused).

UI event handlers

Subscribe to the shared UI event stream in a menu script. Entity callbacks named on_ui_event or on_ui_<event type> receive an event only when that script's entity ID matches the HUD node ID.

local Events = require("demi.events")
local Scene = require("demi.scene")

function Menu:on_start()
    self.ui_subscription = Events.subscribe("ui_event", function(e)
        if e.type == "press" and e.id == "start" then
            Scene.load("scene://levels/forge")
        elseif e.type == "submit" and e.id == "seed_input" then
            print("seed:", e.text)
        end
    end)
end

function Menu:on_destroy()
    Events.unsubscribe(self.ui_subscription)
end
  • Events.subscribe("ui_event", callback) receives all HUD events. Unsubscribe when the script is destroyed.
  • on_ui_event(e) and typed callbacks such as on_ui_value_changed(e) / on_ui_submit(e) are for a script attached to an entity with the same ID as the emitting HUD node.

Payload fields — only those relevant to the event type are filled:

FieldMeaning
typeWhich event: see the list below.
idNode id that fired.
related_idThe other node in a pair (drag source → drop target).
actionUI action name, if the event came from an action binding.
textCurrent text of a text_input (notably on submit).
sourceWhat caused it: pointer, key, gamepad, …
pointer_idWhich pointer/finger, for multi-touch drags.
x, yPointer position in canvas units.
delta_x, delta_yPointer movement this step (drag/scroll).
valueNumeric value (slider/progress).
checkedToggle state.
cancelledBoolean — the gesture was interrupted.

Event types: value_changed, focus_gained, focus_lost, submit, cancel, pointer_enter, pointer_exit, press, release, drag_start, drag, drag_end, drop, scroll.

Always branch on e.type first — a press has no value, and a scroll has no checked. The same stream is available by event type with Events.subscribe("ui_submit", …).

Tweens

A tween animates one property over time. Use them for fades and slides instead of per-frame setters.

local fade = Hud.find("fade")
local handle = Hud.tween(fade, "opacity", 0.0, 0.35)  -- handle, property, target, seconds
Hud.tween(Hud.find("panel"), "y", 120, 0.25)
Hud.cancel_tween(handle)     -- takes the tween handle
Hud.set_reduced_motion(true)
  • Hud.tween(node, property, target, duration) animates property to target over duration seconds. The node is a find/create handle. Animatable properties: opacity, x, y, scale. Returns the tween handle plus an error string.
  • Hud.cancel_tween(handle) stops the tween with that handle. Returns a boolean.
  • Hud.set_reduced_motion(flag) takes a boolean and honors the player’s accessibility preference — with it on, presentation changes finish immediately instead of animating.

The fade line takes the node’s opacity to 0 over 0.35 seconds. Call set_reduced_motion from your options screen and keep honoring it.

Runtime create, clone, remove, reparent

Nodes can be built while the game runs — inventory rows, chat lines, score popups. Hud.create accepts the flat fields listed in the HudNodeDefinition stub; create children separately.

local Hud = require("demi.hud")

function Inventory:refresh(items)
    Hud.clear_children("list")

    for i, item in ipairs(items) do
        local row = Hud.create("list", {
            id = "row_" .. i,
            type = "panel",
            text = item.name,
        })

        if row then
            Hud.clone(row, "compare_" .. i, "compare_panel")
        end
    end
end

local kids = Hud.children("list")   -- array of child ids
local row = Hud.find(kids[1])       -- handle for the first row
if row then
    Hud.remove(row)                 -- delete one row
end
  • Hud.create(parent, def) builds a node from a definition table under parent (id string). Returns the new node’s handle plus an error string.
  • Hud.clone(source, new_root_id, parent?) duplicates a node and its subtree, renaming the clone’s root to new_root_id and placing it under parent (or the source’s parent when omitted). Returns the clone’s handle plus an error string.
  • Hud.reparent(node, parent) moves a node under a new parent. Returns ok plus an error string.
  • Hud.children(parent) returns an array of child ids.
  • Hud.clear_children(parent) removes every child. Returns ok. Handles to those children become stale.
  • Hud.remove(node) deletes a runtime node. Returns ok. Its handle becomes stale.

This spawn-list pattern rebuilds the rows from data each refresh. Keep the returned handles if you will mutate rows later; otherwise ids from your data are enough to find them again.

Virtualization for long lists

A chat log or inventory with hundreds of rows should not build hundreds of nodes. Virtualization computes the visible range and keeps only those rows alive.

local first, count = Hud.visible_range(500, 48, 240, 480)
-- first → one-based index of the first visible row; count → live rows needed

local layout = Hud.virtual_layout({ 48, 64, 48 })
-- layout:visible_range(scroll_offset, viewport_extent, overscan?)
-- layout:set_extent(index, extent), layout:item_offset(index),
-- layout:item_count(), layout:total_extent()

local rows = Hud.recycle_rows("inventory", template, keys, extents,
                              scroll_offset, viewport_extent)
Hud.clear_recycled_rows("inventory")  -- release the pool
  • Hud.visible_range(item_count, item_extent, scroll_offset, viewport_extent, overscan?) covers uniform rows: it returns the one-based first visible item and how many live rows are needed.
  • Hud.virtual_layout(item_extents) takes an array of row extents and returns a persistent layout object for rows with different heights: visible_range(scroll_offset, viewport_extent, overscan?) (one-based first item, live count, leading extent, total extent), set_extent(index, extent) (update one measured row), item_offset(index), item_count(), and total_extent().
  • Hud.recycle_rows(collection_id, row_template, keys, extents, scroll_offset, viewport_extent, overscan?) combines that range with a hidden project-authored row template and returns generation-checked row bindings — each with key, index, node, offset, extent, and rebound — plus an error string.
  • Hud.clear_recycled_rows(collection_id) releases that pool (for a full rebuild). Returns whether it was cleared.

Typical loop: on scroll, ask the layout for the visible window, populate only those rows’ text and images from your data array, and let recycle_rows reuse the pool. Skyforge’s 500-line combat log stays smooth because at most a dozen rows exist at once.

Accessibility snapshot

local snapshot = Hud.accessibility_snapshot()
  • Hud.accessibility_snapshot() takes no arguments and returns an array of tables covering the whole document: role, id, parent, label, description, value_text, value, minimum, maximum, x, y, width, height, focused, disabled, checked, focusable, and offscreen.

value is numeric and value_text is its string form. Use it in tests to assert labels and states, and to verify modal focus traps. Authoring fields are covered in HUD nodes.

Next

Create a declarative subtree

Hud.create(parentId, definition) accepts the same node fields as authored HUD JSON, including nested children, docking, anchors, padding, colors, borders and a UI-prefab instance with a single target-based overrides map. It returns a root handle or nil, error. Creation validates and inserts the whole subtree atomically; an invalid or duplicate child leaves no partial root behind. Use an empty parent ID for a top-level node.

local Hud = require("demi.hud")
local handle, error = Hud.create("ui_root", {
    id = "launch",
    prefab = "ui-prefab://orbit/button-primary",
    arguments = {label = "Launch", accent = "#7DCBFF"},
    overrides = {["$root"] = {dock = "bottom", margin = {24, 0, 24, 24}}},
})
assert(handle, error)

Install the referenced package first. Runtime node creation also accepts the older x, y, width and height fields; canonical position and size take precedence. Scripts can use a package factory to construct these ordinary definitions without maintaining a second layout implementation.

Declarative UI actions

Hud.apply_action(name) applies a declared show/hide/focus action and relayouts the HUD, returning whether an action was found. Actions can live on the HUD document or a node's action_effects map. Prefab-local action names and node references are namespaced with their instance; application actions without a local declaration retain their names. Node-owned actions clone and disappear with their subtree. The editor can preview these same effects without executing Lua.