Game UI
Game UI is everything your players see on screen: health bars, score counters, menus, buttons, and pop-ups. In Renzora you build it with simple .html markup files that update live, then bring it to life with a little Lua.
You don't need to be a programmer to start. If you've ever written a web page, this will feel familiar — and if you haven't, the examples below are short enough to copy and tweak.
Two ways to build UI
Renzora gives you two friendly ways to make game UI, and you can mix them freely:
- Markup — write small
.htmlfiles (therenzora_embersystem). Best for HUDs and menus you want to lay out by hand. Save the file and the UI updates instantly while the game is running. - Canvas widgets — drag ready-made widgets (buttons, sliders, bars) right into your scene in the editor. Best if you'd rather click than type.
Most makers start with markup, so we'll cover that first.
The screenshot below shows the kinds of building blocks you get out of the box — headings, charts, sliders, color pickers, gauges, timelines, and more.

Your first UI file
A UI file is just an .html document with one <template> at the top. Each tag becomes one box on screen. Here's a tiny HUD with a title and a red health bar:
<!-- ui/hud.html -->
<template>
<node
position="absolute" top="24px" left="24px"
flex_direction="column" row_gap="8px"
padding="16px" width="280px"
background="#11151C" border_radius="12px"
>
<text font_size="18" font_color="#FFFFFF">Vitals</text>
<node width="100%" height="14px" background="#1B2233" border_radius="999px">
<node name="health_fill" width="72%" height="100%" background="#E74C3C" border_radius="999px" />
</node>
</node>
</template>
Save it, and the panel appears. Change a color or a width, save again, and it updates without restarting. That fast loop is the whole point of markup.
Selecting a UI entity opens its .html in the built-in code editor, and pressing Ctrl+S there hot-reloads every canvas using that template right away — no need to re-run the game. (Editing the same attributes through the inspector updates the entity live without a rebuild, so your selection stays put.)
The tags you'll use
A handful of tags cover almost everything:
| Tag | What it's for |
|---|---|
<node> | A box — your main layout building block |
<text> | Words on screen |
<image> | A picture (src="...") |
<button> | Something the player can click |
<input> | A text field the player types into |
<icon name="..."> | A small icon, e.g. <icon name="check" /> |
There are a few more tags for repeating lists and reusing components. See the Scripting API for the full list.
Sizes and spacing
Lengths are written like CSS: 12px, 50%, auto, or the viewport units
vw / vh / vmin / vmax. A bare 0 is fine and means 0px.
padding, margin, border and border_radius take one, two or four values:
| Form | Meaning |
|---|---|
padding="10px" | all four sides |
padding="10px 20px" | horizontal, then vertical |
padding="5px 10px 5px 10px" | top, right, bottom, left |
Note the two-value form: it is horizontal first, which is the opposite way round from CSS. The four-value form follows CSS exactly.
Making the UI show live values
The best part: your UI can show numbers that change as the game runs. Wrap a value in double braces and it re-reads every frame:
<text font_size="14" font_color="#FFFFFF">Score: {{ Player.score }}</text>
<text font_size="12" font_color="#8A93A2">Lives: {{ Player.lives }}</text>
What goes inside the braces? A few common forms:
{{ score }}— a variable from the script on the same entity as this UI.{{ Player.score }}— a variable on the entity you namedPlayer.{{ Name }}— the entity's name.
So when your Lua script changes score, the text on screen changes too. You don't have to do anything else.
Show or hide things
Use show= with a condition to flash a warning or reveal a menu only when it matters:
<node show="{{ Player.Health.current < 25 }}" background="#E74C3C" />
<text show='{{ Player.team == "red" }}'>RED TEAM</text>
Conditions understand and, or, not, comparisons (< > <= >= == !=), and parentheses.
Buttons that do something
To make a button run code, give it an on_press name:
<button name="btn_play"
padding="14px" background="#1B1F27" border_radius="8px"
on_press="press_play" on_enter="hover_play">
<text font_color="#FFFFFF">Play</text>
</button>
Then catch that name in your Lua script's on_ui function:
function on_ui(name, args, entity)
if name == "press_play" then
start_game()
elseif name == "hover_play" then
play_sound("audio/menu_button.mp3", 0.5)
end
end
on_press runs on click; on_enter and on_exit run when the mouse moves over or away. UI event handling like this is Lua-only.
Gauges, bars, and charts
Need a circular gauge, a bar chart, or a speedometer? Add vector="..." to a node and Renzora draws it for you:
vector= | Draws |
|---|---|
gauge (or arc, ring) | A circular gauge |
bars | A bar chart |
line (or chart) | A line chart |
wave | A waveform |
speedometer (or dial) | A full dial with ticks, labels, and a needle |
<node vector="gauge" width="160px" height="160px"
value="{{ Player.fuel }}" min="0" max="100"
color="#4C8BF5" readout="{{ Player.fuel }}" />
The value, data, and readout fields accept live {{ }} bindings, so a fuel gauge or speedometer tracks your game in real time. For the full list of widget options, see the Scripting API.
Showing and hiding UI from a script
Your script spawns and hides UI with action(...). The common verbs:
| Verb | What it does |
|---|---|
hui_spawn | Show a UI file, e.g. { template = "ui/hud.html" } |
hui_despawn | Remove a UI file |
hui_hide / hui_show | Hide or show a named piece of UI |
quit | Close the game |
function on_ready()
-- Show the HUD as soon as this entity wakes up
action("hui_spawn", { template = "ui/hud.html" })
end
function open_pause_menu()
action("hui_hide", { name = "hud_root" })
action("hui_spawn", { template = "ui/pause_menu.html" })
end
These action() verbs are Lua-only.
Building UI by dragging in the editor
Prefer clicking to typing? The second path lets you drop ready-made widgets straight into your scene and arrange them in the viewport. Below, a match screen is being assembled from widget cards on the left, with their colors and values edited on the right — and a big "START MATCH" button laid out in the center.

Each widget is an entity that lives in your scene and saves with it, so it's there next time you open the project. There are widgets for the usual things — buttons, sliders, checkboxes, dropdowns, text inputs, progress and health bars, tooltips, modal pop-ups, and basic shapes.
When you add a canvas yourself — Add Entity → UI Canvas, or the New UI scene starter — a blank ui/<name>.html is created alongside it and linked as its template, so selecting the canvas opens that file in the code editor. A canvas that appears on your behalf, to host something you dropped into an empty scene, doesn't get one: it keeps the template or widget you dropped, rather than adding a second, empty template file to your project.
Widgets always live under a UI Canvas. The canvas is what scopes its widgets to the game view; a widget outside one has nowhere to render. So the editor keeps that relationship intact for you: if you drag a widget out to the scene root (or under a non-UI entity), it's automatically re-homed under a fresh UI Canvas rather than escaping into the editor's own interface — you'll simply see a new canvas appear in the hierarchy holding it. Having more than one canvas is fine (a HUD and a pause menu, say). The reverse is also enforced: a canvas can't become a child of a widget — drop one there and it pops back to the top level.
Canvas scaling
You design against a fixed size — Ref Width and Ref Height on the canvas, 1280 × 720 by default. Players don't have that window. Scale Mode decides what happens in between:
| Mode | What it does |
|---|---|
| Fit (default) | Lays the canvas out at exactly the reference size and scales that whole box to fit the window, centred, with bars on the leftover axis. What you composed is what ships. |
| Expand | Scales text and padding, but lets the canvas fill the window so the layout re-flows to the real aspect ratio. |
| Constant | No scaling. One authored pixel is one screen pixel. |
The difference only shows up once a canvas holds more than one thing. A single centred panel looks the same in every mode; a centred panel and a bottom-left dialogue box do not — under Expand each is resolved against the live window, so widening it walks them apart, while under Fit the whole design box moves as one piece.
So: Fit for menus, dialogue, anything you laid out by eye. Expand for a HUD whose corners are meant to hug the screen edges however wide it gets. Constant for pixel-art UI that must not resample.
Fit is what the UI editor shows you, which is why it's the default — the editor renders at the reference size, so the panel and the game agree. World UI Panels ignore the setting entirely: their surface is the reference resolution, so there's nothing to reconcile.
UI in the 3D world
A canvas draws its UI flat across the screen. A World UI Panel draws the same kind of .html template onto a flat surface inside the scene — a monitor on a wall, a control terminal you walk up to, a floating menu in VR. It's a real 3D object: it has a position and rotation, it's lit by the scene, and things can pass in front of it.
Add one with Add Entity → UI → World UI Panel. It comes with a starter template so you can see it straight away; the panel carries an HTML Template field just like any other UI entity, so point it at any of your .html files (in the inspector, or by editing the file it created) and that template is what appears on the surface. Everything else about authoring is identical — the same tags, the same live {{ Component.field }} values, the same hot-reload on Ctrl+S.
Two settings are specific to the panel, both in the inspector:
- Size — how big the surface is in the world, in meters (width × height).
- Resolution — how many pixels the template is drawn at before it's mapped onto the surface. Higher is crisper but costs more; the default (1280×720) suits a wall-sized panel.
Panels are interactive, not just decorative. Point at one and click — with the mouse in the editor viewport, or with a controller in VR — and the buttons, sliders and hover states respond exactly as they do on a normal canvas. Aiming anywhere on the surface maps to the matching spot on the template, so a button in the top-right of the file is a button in the top-right of the panel.
Unlike a canvas, a panel doesn't hold widget entities — its content is entirely the template it points at, so you build it by editing the .html, not by dragging widgets onto it.
Controlling those widgets from a script
Widgets respond to a set of ui_* verbs. You target a widget by the name you gave it in the editor:
action("ui_set_text", { name = "score_label", text = "Score: " .. score })
action("ui_set_slider", { name = "volume", value = 0.5 })
action("ui_set_theme", { theme = "light" })
For anything without a dedicated verb (like driving a health bar's fill), use set_on:
-- Fill a bar named "health_fill" from 0.0 to 1.0
set_on("health_fill", "UiBarFill.value", current_hp / max_hp)
The built-in themes are dark (default), light, and high_contrast. Color values in ui_set_color are floats from 0.0 to 1.0, not 0–255. The full verb list lives in the Lua reference.
Putting it together: a scripted HUD
Here's a small, complete script that spawns a HUD, keeps the health bar filled, and switches to a game-over screen when health hits zero:
-- hud.lua — attach this to one entity in the scene
function props()
return {
max_health = { value = 100, hint = "Player max HP" },
_hp = { value = 100, hint = "Current HP" },
_score = { value = 0, hint = "Score" },
}
end
function on_ready()
action("hui_spawn", { template = "ui/hud.html" })
_hp = max_health
_score = 0
end
function on_update()
-- ui/hud.html shows {{ _hp }} and {{ _score }} directly,
-- and we size the health bar from the current fraction.
set_on("health_fill", "UiBarFill.value", _hp / max_health)
if _hp <= 0 then
action("hui_despawn", { template = "ui/hud.html" })
action("hui_spawn", { template = "ui/game_over.html" })
end
end
-- Buttons in the markup route here by their on_press="..." name
function on_ui(name, args, entity)
if name == "restart" then
_hp = max_health
_score = 0
end
end
UI scripting (
action(),set_on/get_on, andon_ui) runs through the script VM — write your UI logic in.lua.
See also
- Scripting Overview — backends, hooks, and the
action()escape hatch. - Lua reference — the full function catalog, including
set_on/get_on. - Scripting API — the complete UI tag, binding, and widget reference.