Pausing, quitting, settings and screens#
A place draws its own pause menu, its own settings screen and its own loading screen instead of
Minecraft's. The globals that do it are settings and ui, along with a few members of game. They
read and write one player's own game, so you use them from client/ scripts, the same place camera
and input live.
Escape and pausing#
game.pauseRequested is a signal that fires when the player presses Escape, or when the
game window loses focus. You connect to it to open your own menu:
local menu = world:find("PauseMenu") :: ScreenGui
game.pauseRequested:connect(function(reason)
-- reason is "escape" or "focusLost"
if reason == "focusLost" and menu.enabled then return end
menu.enabled = not menu.enabled
game.paused = menu.enabled
if menu.enabled then input:releaseMouse() else input:lockMouse() end
end)reasonis the string"escape"or"focusLost", so you can treat the two differently. The line above leaves an already open menu alone when the player alt-tabs away, and only toggles it on a real Escape.game.paused = truefreezes the world the way the vanilla pause menu does: the server stops ticking, and bodies and physics stop. Client scripts keep running, so your menu can still animate. It only freezes a single player game. On a server with other players, writing it does nothing.input:releaseMouse()gives the pointer back so the player can click the menu, andinput:lockMouse()hands it to the camera again when the menu closes.
With no script connected, Escape opens the pause menu as before: Minecraft's own while you
are developing, and a plain Resume / Settings / Quit menu in an exported game. The pauseMenu feature
in place.toml turns that off entirely.
If any script is connected to game.pauseRequested, the vanilla pause menu does not open. Your
connection is the pause menu, so if it does not show anything, Escape appears to do
nothing.
Quitting#
game:quit() leaves the place. Where the player ends up depends on how the place is running:
game:quit()- In an exported game, the game closes.
- In a place started from the command line with
-Pplace, the game closes too. - During a playtest in the editor, it goes back to editing.
- In a project opened from the project hub, it leaves to the hub.
The game waits for any window:screenshot still being written before it closes, so a script can take
a picture and quit on the next line.
game.exported is true inside an exported game. Read it when a menu should offer Quit to desktop
in the shipped game and Back to the hub while you work on it.
game:bindToClose(handler) runs something as the place stops, on either side. See
Tweens and timing.
Settings#
settings reads and writes the player's own Minecraft options, so a place can build its own settings
screen instead of sending them to Minecraft's. Changes apply straight away, and settings:save()
keeps them for next time.
settings.fov = 90 -- 30 to 110
settings.sensitivity = 0.5 -- 0 to 1
settings.fullscreen = true
settings.vsync = false
settings.maxFps = 144 -- 10 to 260, 260 is unlimited
settings.guiScale = 3 -- 0 is automatic
settings.renderDistance = 12 -- 2 to 32 chunksVolumes and key binds are lists rather than fixed properties, because Minecraft has several of each:
settings:volumes() -- { "master", "music", "record", "weather", "block", ... }
settings:volume("master") -- 0 to 1
settings:setVolume("music", 0.3)
settings:actions() -- { "forward", "back", "left", "right", "jump", "sneak", "sprint", "attack", "use", "chat", "playerList" }
settings:keyOf("jump") -- "space"
settings:bind("jump", "f") -- a letter, a digit, space, enter, tab, f1 to f12, the arrows,
-- leftshift, rightcontrol..., mousebutton1 to mousebutton3
settings:save()
game:openSettings() -- Minecraft's own options screen, for everything elsesettings:volumes()andsettings:actions()give you the names to build a screen out of, so you can loop over them instead of writing a row per option by hand.settings:keyOf(action)tells you what a Press a key row should currently show, andsettings:bind(action, key)changes it.game:openSettings()opens Minecraft's own options screen. Use it for everythingsettingsdoes not cover, rather than rebuilding all of Minecraft's options.
The core interface#
ui turns the pieces of Minecraft's own interface on and off for this client, while the place runs.
-- client
ui.setCoreGuiEnabled("hotbar", false)
ui.setCoreGuiEnabled("all", false)
print(ui.getCoreGuiEnabled("chat"))| Name | What it hides | The [features] switch it flips |
|---|---|---|
health |
the hearts | healthBar |
hunger |
the drumsticks | hungerBar |
hotbar |
the item bar | hotbar |
chat |
the chat window | chat |
playerList |
the Tab list | tabList |
crosshair |
the crosshair | crosshair |
experience |
the xp bar and level | xpBar |
all |
all seven at once |
- A name that is not one of these is an error that lists the ones that are.
getCoreGuiEnabled("all")is true only while every one of the seven is on. One hidden piece makes it false.- Reloading the place puts every one it touched back the way
place.tomlasked for it.
These are the same switches [features] in place.toml sets. Use place.toml to say what the
place looks like for everyone, and ui to change it for one client, for a cutscene or a menu. See
Places.
Notifications#
ui.sendNotification shows a toast in the top right corner of this client's screen:
-- client
ui.sendNotification({
title = "Wave 3",
text = "Hold the gate",
icon = "res://ui/wave.png",
duration = 8,
button1 = "Ready",
button2 = "Later",
callback = function(button) print(button, "was pressed") end,
})The toast fades over its last second and goes after duration seconds, 5 by default. Several stack
down the corner in the order they were sent.
| Key | ||
|---|---|---|
title |
required | one line |
text |
optional | wraps under the title |
icon |
optional | a res:// image, drawn 32 by 32 on the left |
duration |
optional | seconds, more than 0 |
button1, button2 |
optional | a button each, along the bottom |
callback |
optional | gets the text of the button that was pressed |
- A notification with no title is an error, and so is a duration of 0 or less.
- The callback only runs when a button is pressed, and it is handed the button's text, so the example
above prints
ReadyorLater. A toast that runs out of time, or that a reload clears, never calls it. - Pressing either button closes the toast.
- The toast is built out of Moud's own
ScreenGui,Frame,TextLabelandTextButtoninstances on a local screen, so it draws over the place's own interface and disappears with it on a reload.
The loading screen#
A place draws its own loading screen with client/loading.luau. It runs on the player's machine the
moment the game knows which place it is starting: before it looks up the server, before it connects,
before there is a world. What it builds is drawn over Minecraft's connecting, loading and saving
screens, and stays until the place is running.
-- client/loading.luau
local world = game.world
local loading = game.loading
local gui = world:add("ScreenGui", { name = "Loading", displayOrder = 100 })
local screen = gui:add("Frame", { size = udim2(1, 0, 1, 0), backgroundColor = color(1, 1, 1) })
screen:add("UIGradient", { startColor = color(0.14, 0.1, 0.28), endColor = color(0.02, 0.03, 0.07), rotation = 90 })
screen:add("TextLabel", {
position = udim2(0.5, 0, 0.5, -50), anchorX = 0.5, anchorY = 0.5,
size = udim2(0, 400, 0, 40), backgroundTransparency = 1,
text = "CRYSTAL RUN", textSize = 24, textColor = color(1, 1, 1),
})
local bar = screen:add("Frame", {
position = udim2(0.5, 0, 0.5, 10), anchorX = 0.5, anchorY = 0.5, size = udim2(0, 300, 0, 6),
backgroundColor = color(1, 1, 1), backgroundTransparency = 0.85, cornerRadius = 3,
}) :: Frame
local fill = bar:add("Frame", { size = udim2(0, 0, 1, 0), backgroundColor = color(0.55, 0.8, 1), cornerRadius = 3 })
local status = screen:add("TextLabel", {
position = udim2(0.5, 0, 0.5, 22), anchorX = 0.5, size = udim2(0, 400, 0, 16),
backgroundTransparency = 1, textSize = 9, textColor = color(0.75, 0.8, 0.95),
}) :: TextLabel
local words = {
lookup = "Finding a server",
connecting = "Connecting",
joining = "Joining",
world = "Loading the world",
ready = "Ready",
}
local function show()
fill:tween({ size = udim2(loading.progress, 0, 1, 0) }, { time = 0.2 })
status.text = words[loading.phase]
end
loading.changed:connect(show)
show()
local panel = screen:add("Frame", {
position = udim2(0.5, 0, 0.5, 30), anchorX = 0.5, anchorY = 0.5, size = udim2(0, 300, 0, 90),
backgroundColor = color(0, 0, 0), backgroundTransparency = 0.4, cornerRadius = 6, visible = false,
}) :: Frame
local reason = panel:add("TextLabel", {
position = udim2(0, 12, 0, 10), size = udim2(1, -24, 0, 40), backgroundTransparency = 1,
textColor = color(1, 1, 1), textWrapped = true,
}) :: TextLabel
local retry = panel:add("TextButton", { position = udim2(0, 12, 1, -34), size = udim2(0.5, -16, 0, 24), text = "Try again" }) :: TextButton
local quit = panel:add("TextButton", { position = udim2(0.5, 4, 1, -34), size = udim2(0.5, -16, 0, 24), text = "Quit" }) :: TextButton
local update = false
loading.failed:connect(function(why, kind)
update = kind == "update"
reason.text = why
retry.text = if update then "Download" else "Try again"
panel.visible = true
bar.visible = false
status.visible = false
end)
retry.activated:connect(function()
if update then
loading:openDownload()
return
end
panel.visible = false
bar.visible = true
status.visible = true
loading:retry()
end)
quit.activated:connect(function()
loading:quit()
end)- The script has its own
game.world, which only it sees. Nothing in it is sent to the server, and the place's scripts do not find it in their tree. It has the interface classes, tweens,task,Sound,VideoFrame,requireandwindow. It has no players, no remotes and nocameraorinput: none of them exist yet. - It shows the moment Minecraft has loaded its resources. Minecraft's own fade out is skipped, so a server that answers fast never fails behind the Mojang screen.
task.wait,task.delayand tweens count real seconds, so they keep time while the window is in the background and drawing slowly.loading.changedfires whenever the phase, the progress, the message or the server changes, so the bar above follows along without polling.- Connecting to
failedmeans the script draws the failure itself: the engine's own screen with Try again and Quit is not shown. The pointer is free on a failure, so the buttons can be clicked. A script that does not connect to it keeps the engine's screen. - A script that errors before it finishes its first run is stopped, and the
[loading]look below takes its place.
game.loading is there in client/main.luau too:
| Member | |
|---|---|
phase |
"lookup" while the server lookup is read, "connecting" while it connects to a server or starts its own world, "joining" while it logs in, "world" while the world and the place arrive, then "ready" |
progress |
from 0 to 1. It follows the world loading where Minecraft reports it, and steps with the phase otherwise |
message |
the lookup's message, or why it failed |
server |
the address being tried, empty when there is none |
hold |
false by default. See below |
changed |
fires when any of the above changes |
ready |
fires once the place is running |
failed |
fires with the reason and what went wrong: "connect", "update", "closed" or "disconnected" |
finish() |
takes the loading screen away |
retry() |
looks the server up and connects again, after a failure |
quit() |
closes the game |
openDownload() |
opens the lookup's download page in the browser |
The reason is short and plain, like Connection refused, Timed out or The server is full. What
Minecraft said exactly is in the log. A kick keeps the text the server gave it.
Keeping it up until the game is ready#
When the place is running, the loading screen fades out and its script stops. A game that wants to
load more first, or play an intro under it, sets hold, and takes it away itself with finish():
-- client/loading.luau
game.loading.hold = true-- client/main.luau
local music = game.world:add("Sound", { soundId = "res://sounds/theme.ogg", looped = true }) :: Sound
music.loaded:once(function()
game.loading:finish()
music:play()
end)finish() fades the loading screen out over less than half a second, then stops its script. Until
then it is drawn over the game. A place that sets hold and never calls finish() stays on its
loading screen.
When the player is disconnected later on, the script starts again and gets failed with
"disconnected", so the same panel shows why and offers to rejoin.
Without a script#
A place with no loading script describes its loading screen in place.toml instead:
[loading]
background = "res://ui/loading.png" # covers the screen, cropped to fit
color = "#12151c" # behind the background, and alone without one
logo = "res://ui/logo.png" # in the middle; without one the place name is shown
text = "Loading" # animated dots are added
tips = ["Crystals respawn after 10 seconds", "Double jump off walls"] # one every 5 seconds
preload = ["res://ui/**"] # downloaded before a player joins the published gameIt replaces Minecraft's world loading, connecting and saving screens. While you are developing it
shows once a [loading] table exists. An exported game always uses it, with the defaults when the
table is missing. With a loading script, only color is used, as the colour behind it. preload is about
the published game's downloads, see Files players get first.
Being kicked or disconnected#
The server decides who stays. Both calls take the reason the player is shown:
-- server
player:kick("The round is over")
player:ban("Flying", 3600)ban takes the reason and a number of seconds. It also writes the player into the Minecraft server's
ban list, so they cannot come straight back. See
Players and bodies.
In an exported game, a disconnected player sees the reason over the loading look, with Try again
and Quit, instead of Minecraft's screen. A loading script connected to
game.loading.failed draws that screen itself.
Screens an exported game never shows#
An exported game starts straight in its place. It never shows Minecraft's title screen, singleplayer world list, multiplayer server list or Realms. Leaving the place closes the game.
