Appearance
Menus and HUDs
Panels, prompts and heads-up displays. Everything here is drawn on the player's own machine, so navigation and toggles are instant.
The complete catalogue of nodes and style keys is in the HUD reference. This page is how to use them.
A panel that appears on click
luau
--!strict
local self = require("self")
self:on_touch(function(p)
p:show_hud({
id = "sign",
anchor = "center",
modal = true,
pages = { main = {
{ "text", "Mind the step", size = 22, bold = true },
{ "text", "The floor below is being repaired.", wrap = true },
{ "button", "Understood", close = true },
} },
})
p:show_cursor()
end)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
show_cursor reveals the pointer and freezes the avatar. Esc always closes what a script opened — the player owns their pointer, and no script can take it.
The text of a node comes second, by position. { "text", "Hello" } works; { "text", text = "Hello" } creates an empty node that takes up space and draws nothing. This is the most common HUD mistake.
Several pages
Page changes never touch your script:
luau
p:show_hud({
id = "menu",
anchor = "center",
modal = true,
start = "main",
pages = {
main = {
{ "text", "Workshop", size = 22, bold = true },
{ "button", "Tools", goto = "tools" },
{ "button", "Close", close = true },
},
tools = {
{ "text", "Tools", size = 18, bold = true },
{ "button", "Take a hammer", emit = "take", data = { item = "hammer" } },
{ "button", "Back", back = true, style = "ghost" },
},
},
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Only emit costs a message. goto, back, close, set and add are all local.
Receiving a button
luau
local world = require("world")
self:on_hud(function(p, event, data)
if event ~= "take" then return end
world:spawn({
model = tostring(data.item),
hold = p.id,
at = "right_hand",
life = 0,
})
end)1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Pressing a button on your HUD counts as interacting with your object, which is why handing something over from a menu is allowed.
Trust event and the player, not data. data is whatever the button carried; check it before using it.
A live number
Do not rebuild the HUD to change a value — that would throw away which page they were on. Push it:
luau
--!strict
local self = require("self")
local players = require("players")
local score = {}
local function show(p)
p:show_hud({
id = "score",
anchor = "top_right",
size = { 160, 0 },
vars = { points = score[p.id] or 0 },
pages = { main = {
{ "text", "Score: {points}", size = 18, bold = true },
} },
})
end
players:on_enter(function(p)
score[p.id] = 0
show(p)
end)
local function award(id: string, n: number)
score[id] = (score[id] or 0) + n
local who = players:get(id)
if who then
who:update_hud("score", { points = score[id] })
end
end1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{points} in the text is replaced by the variable. update_hud merges by key, so keys you do not mention keep their values.
A prompt hanging in the world
Set world and the panel becomes a billboard on an object rather than a screen corner:
luau
p:show_hud({
id = "shop_sign",
world = { height = 2.2, distance = 12, fade = 18, gaze = 0 },
size = { 220, 0 },
pages = { main = {
{ "text", "General store", size = 16, bold = true, align = "center" },
} },
})1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
| Key | Meaning |
|---|---|
height | metres above the object |
distance | stop drawing past here |
fade | start dissolving here |
gaze | only while looked at, within this many degrees. 0 = any angle |
gaze = 0 is right for a sign, which should be readable from anywhere. gaze = 20 is right for a pickup prompt, which should only appear for the thing you are actually looking at.
Hold to confirm
luau
vars = { fill = 0 },
pages = { main = {
{ "ring", var = "fill", size = 60, thickness = 4 },
{ "text", "Hold F to detonate", size = 12, align = "center" },
{ "hold", key = "f", dur = 2.0, var = "fill", emit = "boom" },
} },1
2
3
4
5
6
2
3
4
5
6
The hold node draws nothing — it drives the variable that the ring shows. Nothing is sent while the key is held; only the completed emit arrives.
Showing it to everyone nearby
luau
local players = require("players")
for _, p in ipairs(players:nearby(20)) do
p:show_hud(WARNING)
end1
2
3
4
5
2
3
4
5
Styling without repeating yourself
luau
styles = {
title = { size = 22, bold = true },
card = { bg = "#1b2330", rounding = 12, pad = 12, shadow = true },
},
pages = { main = {
{ "text", "Workshop", class = "title" },
{ "column", {
{ "text", "Everything here is free." },
{ "button", "Take one", emit = "take" },
}, class = "card" },
} },1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
A node's own key always beats its class.
Hiding parts conditionally
luau
vars = { coins = 4 },
pages = { main = {
{ "text", "Not enough coins", visible = { "<", "coins", 10 } },
{ "button", "Buy", emit = "buy", enabled = { ">=", "coins", 10 } },
} },1
2
3
4
5
2
3
4
5
Both are evaluated on the player's machine, so they update the instant a variable changes.
Common mistakes
| Symptom | Cause |
|---|---|
| a label is invisible | text given as text = instead of in position two |
| the HUD never appears | missing id or pages |
| a button does nothing | it has no emit, goto, back, close, set or add |
| the number never updates | rebuilt with show_hud instead of pushed with update_hud |
show_cursor refused | this is a social world; it only works in an experience |
See also
- HUD reference — every node and key
- show_hud · on_hud
