Skip to content

User interface

A game’s menus and HUD are GUI documents: .xml files of markup that say what is on a screen, much like a web page. GUI style sheets, plain .css, say how it looks. Scripts show documents as screens, fill in their text, and answer their buttons.

Make one in the Assets panel: right-click the background and choose New > GUI > GUI Document (the File menu has the same list). Its page is a text editor with a live preview beside it. The game’s own GUI draws the preview with the project’s fonts, theme and sheets, so the preview shows what the game will show. While the markup does not build, the preview keeps the last version that did, and the problems list under the code says what is wrong. Save the document to use it in the game.

A GUI document’s page: its markup on the left with the problems list under it, and the live preview of the HUD it describes on the right.

You can also import an .xml file you wrote elsewhere.

A pause menu:

<Screen class="fades" nav-default="resume-btn" back="none">
<Panel class="centre">
<Panel class="card">
<Label class="title" text="Paused"/>
<Button id="resume-btn" text="Resume"/>
<Button id="restart-btn" text="Start the level over"/>
<Button id="title-btn" text="Leave to the title"/>
</Panel>
</Panel>
</Screen>

Elements nest. A Panel holds other elements and lays them out, and controls are what the player sees and uses. An element’s attributes are its settings:

  • id names it, so a script can find it, and class gives it classes for the style sheets.
  • An attribute that is one of the element’s own properties (text, value, min, max, src) sets that property.
  • Any other attribute is a CSS property for that element alone, such as gap="8", flex-direction="row" or font-size="24px". It takes precedence over the style sheets.
  • {{name}} in a value follows the GUI’s data value of that name, which a script sets with gui.set (below): text="Score: {{score}}", value="{{health}}". A slider, check box or text field bound this way writes the player’s changes back to the value.

An element or attribute the GUI does not know is left out, and the page’s problems list warns about it.

A document that a script shows as a menu or HUD starts with <Screen>. Its attributes say how it sits over the screens below it:

Attribute Effect
covers="true" The screens below are hidden. Use it for a title screen. Off by default.
blocks="false" The screens below, and the game, still take input. Use it for a HUD. Blocking is on by default.
pauses="true" The screen stack reports the game as paused while the screen is up; the script decides what pausing means. Off by default.
back="none" Escape and the pad’s East button do not pop it, so the script handles going back. By default they pop the top screen, unless it is the only one.
nav-default The id of the element a keyboard or gamepad starts on, so menus work without a mouse.

A button activates on a click, Return, Space or the pad’s South button. The arrow keys and the pad move the focus between controls.

Containers Panel, ScrollView, ScrollBar, Splitter, Expander
Text Label, RichText, EditableLabel
Text input TextField, TextArea, NumberField
Buttons Button, CheckBox, RadioButton, ToggleSwitch
Values Slider, ProgressBar, ComboBox, ColorSwatch, ColorPicker
Pictures Image
Lists ListBox, ListView, TreeView, TreeItem, TableView, TableItem
Tabs TabView, TabBar, Tab, TabPage
Menus and windows MenuBar, Menu, MenuItem, MenuSeparator, Window, MessageBox, ToastHost
Spacing Separator, Spacer
Game screens Screens, Screen, Prompt

An Image shows a texture or a vector image (an imported .svg) by its ID: <Image src="{6faf9764-c33a-444f-8126-91cc8eade4f5}"/>. A script can set it instead (Image.of(node).src = "{...}"). In that case leave src out of the markup, because a markup attribute acts as a style that the element keeps returning to. A picture that is still loading when it is set appears once it is ready.

A Prompt shows the control for one of the input map’s actions, such as <Prompt action="Pause"/>. It shows a pad button while a pad is in use and a key otherwise, and follows the player’s rebinding. It shows the control’s name as text, and tells the style sheet which control it is, so a sheet can draw a picture for each. Its binding attribute is the control’s name in lower case with dashes (pad-south, pad-lb, pad-ls for the left stick pressed, space, w-a-s-d), and its kind is pad, keyboard or pointer:

prompt[kind="pad"] { color: rgba(0, 0, 0, 0); width: 38px; height: 38px; background-size: 38px 38px; }
prompt[binding="pad-south"] { background-image: url("{a-texture-of-the-A-button}"); }

A GUI style sheet (New > GUI > GUI Style Sheet) is CSS. Rules pick elements and set how they look:

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
screen.fades:entering { animation: fade-in 0.3s ease-out; }
.centre { flex-grow: 1; justify-content: center; align-items: center; padding: 32px; }
.card { padding: 24px; gap: 10px; width: 400px; background-color: rgba(12, 9, 7, 0.91); border-radius: 16px; }
.title { font-size: 36px; color: #F0D8A8; }
button { height: 44px; font-size: 22px; color: #F0E0C0; background-color: #1E1610; border-radius: 10px; }
button:hover { background-color: #2C2016; }
button:focus-visible { border: 4px solid #F0C070; }
button:disabled { color: #6A5A48; }
  • Selectors: an element’s type in lower case (button, label, slider, screen; a RadioButton is radio and a ToggleSwitch is switch), .class, #id, [attribute] and [attribute=value], and the combinators (space, >, +, ~). States are pseudo-classes: :hover, :active, :focus, :focus-visible, :focus-within, :disabled, :checked, :selected, and the structural ones (:first-child, :nth-child() and the rest), with :not(), :is() and :where(). A screen is :entering while it arrives and :leaving while it goes.
  • Layout is flexbox by default: every container is a column unless flex-direction says row, with gap, justify-content, align-items, flex-grow and the rest. display: grid lays out a grid, and display: none removes an element from the layout.
  • Appearance: sizes, margin, padding, background-color, background-image, border, border-radius, box-shadow, opacity, transform, filter, backdrop-filter, the font and text properties, and text-shadow, text-outline and text-glow for text that has to stay readable over a busy scene.
  • Motion: transition, and animation with @keyframes.
  • Variables: --name custom properties, read with var(--name). The GUI’s built-in theme is written in them (--accent, --surface, --text and more), so a sheet that sets them on :root restyles every control at once.
  • At-rules: @media, @import and @font-face besides @keyframes.

A sheet’s page edits the sheet beside a preview. The preview draws markup that belongs to the page, shown and editable in the Previewed Over pane above the preview. It starts with a little of every control. Preview Over a Document in the page’s toolbar replaces it with a copy of one of your documents; the document itself is not changed.

Set the project’s sheets in Project > Project Settings…, in the UI section, as GUI sheets. They apply in order over the built-in theme, each over those before it, to everything the game shows.

Import .ttf, .otf or .ttc fonts. The UI section of Project Settings has the Default UI font, used where nothing names another (Roboto when it is empty), and Other UI fonts for more families. A sheet picks one with font-family, by the Family set on the font asset:

.title { font-family: "Lilita One"; font-size: 60px; }

The gui facade reaches the game’s GUI. gui.root() is the screen layer, which survives scene changes, and inside it, with the id screens, is the game’s screen stack. Push a document by its ID as a string, with or without braces (right-click the document in the Assets panel and choose Copy GUID):

local PauseDoc = "{f5faae8a-fa1f-4c68-b2af-1cc8c1707afc}" -- a GUI document's ID
local function screens()
return ScreenStack.of(gui.root():find("screens"))
end
function Game:pause()
run.setTimeScale(0)
local menu = screens():push(PauseDoc)
local this = self
menu:find("resume-btn"):on("click", function(e)
this:resume()
return true
end)
end
function Game:resume()
screens():pop()
run.setTimeScale(1)
end
function Game:showLoot(loot)
Label.of(gui.root():find("hud-loot")).text = "Loot " .. loot
ProgressBar.of(gui.root():find("hud-light")).value = 0.75
end
  • The stack’s push(document) shows a document on top and returns its screen. pop() removes the top screen, replace(document) swaps it for another, back() goes back as Escape does, and clear() empties the stack. top() is the top screen and count() how many there are; isPaused() is true while a screen with pauses is up. A document is loaded fresh on each push, so connect its buttons each time.
  • find(id) finds an element in a screen, or anywhere under gui.root(). It returns nil (Luau) or null (AngelScript) when there is none. A screen that was popped stays in the tree while its exit animation plays, so search the new top screen first when ids repeat. Reach a control’s own properties through its type: Label.of(node).text, Slider.of(node).value, Button.of(node) (Label::of and so on in AngelScript).
  • Every element has visible, enabled and opacity, and displayed, which takes it out of the layout when false (a hidden element keeps its place). UIWidget.of(node) adds addClass, removeClass, hasClass and toggleClass, so a sheet can restyle it.
  • on(event, handler) calls the handler on each click (a button, from the mouse, keyboard or pad), input (a slider as it moves), change, focus and other events. A handler that returns true takes the event, so it goes no further.
  • tween(property, to, seconds, easing) animates a property, named as a sheet names it: node:tween("opacity", "0", 0.5, "ease").
  • gui.set(name, value) sets a data value that markup follows with {{name}}, and gui.getNumber, gui.getText and gui.getBool read one back.

The GUI runs on real time, so menus still animate while setTimeScale(0) holds the game still.

Three components show a document from an entity instead. Each has a Document, and an optional Sheet that applies over the project’s sheets to that component alone.

GUI Canvas shows its document while its entity is in the scene: a HUD that belongs to a level. Order puts one canvas above another (higher on top), and with Interactive off it takes no input. Scaler Mode sizes it: ConstantPixel (the default) keeps one GUI pixel to one screen pixel, and ReferenceResolution lays the document out at the Reference Resolution (1920 by 1080 by default) and scales it to fit, so it looks the same on every display. With Render Mode RenderTexture it draws into a texture of Render Texture Width by Render Texture Height (512 by 512 by default) instead, which a Sprite or Decal on the same entity shows, for example a computer screen in the world.

GUI Billboard floats a document over the entity, such as a name or a health bar, hung by its bottom centre and facing the camera. Offset moves it from the entity: with Orientation Cylindrical (the default) the offset is in world space, and with Screen it is in the entity’s own space, so it turns with the entity. With Scale Mode Distance it shrinks as the entity gets further away; it is full size at Reference Distance (10) and kept between Min Scale (0.3) and Max Scale (2). The default, Fixed, keeps one size at any distance.

GUI World Panel puts a document on a flat panel in the entity’s plane, Size Meters across (1.6 by 0.9 by default), drawn at Pixels Per Meter (200). The panel is shown through a Sprite on the entity, which is added if the entity has none. With Interactive on (the default), the player can click it.

Each has Visible. A script reaches a component’s document through its root(), which is nil (Luau) or null (AngelScript) until the document is built:

local board = GuiBillboardComponent.of(self.entity)
board.visible = true
local panel = board:root()
if panel then
ProgressBar.of(panel:find("meter")).value = 0.5
Label.of(panel:find("mark")).text = "?"
end

GuiCanvasComponent and GuiWorldPanelComponent work the same way.

A document set as Loading screen in the Startup section of Project Settings shows while the game loads its first scene. The GUI’s data value bootProgress goes from 0 to 1 as it loads, so a bar can follow it:

<Panel class="loading">
<Label text="Loading"/>
<ProgressBar value="{{bootProgress}}"/>
</Panel>

If it is left empty, the engine shows its own: the engine’s logo over a progress bar and the word “Loading”.