Moud

Getting started#

This page walks through a whole place, from the project hub to a floor you can stand on and a camera that switches to third person when you sneak. It takes two files and no setup beyond creating the project.

Create a place#

Start Moud and the title screen opens the project hub. From the repository, that is ./gradlew :mod:runClient with no -Pplace. New project asks for a name, a folder (Moud in your home folder unless you pick another) and one of three starts:

  • Baseplate gives you a 128 m floor to build on. Use it when you want to try something out straight away.
  • Main menu gives you a menu drawn over a camera flying along a CameraPath. Play hands the player a body, Escape opens a pause menu, and place.toml sets a loading screen. Players wait in the menu because the template sets game.players.autoSpawn = false. The menus are built in client/main.luau, and the scene holds the floor, the SpawnLocation, the camera path and the Menu remote.
  • Empty gives you a place with nothing in the scene.

Each template is a folder of ordinary files. Nothing in it is generated at build time, so you can read and change every part of it.

Pick Empty for this page, so the floor you stand on is the one your script makes. Create opens the new place in the editor. Its server/main.luau and client/main.luau only print a line each; the next two sections replace them. Right click the project in the hub and choose Show the folder to find the files, and see Editing scripts in your code editor to set up completion for them.

Write the server script#

server/main.luau runs on the server. It builds the world and decides what happens to the players in it:

luau
local world = game.world

world:add("Part", {
    name = "floor",
    size = vec3(40, 1, 40),
    cframe = cframe(0, 64, 0),
    color = color(0.35, 0.42, 0.38),
    anchored = true,
})

game.players.joined:connect(function(player)
    local body = player:spawn(vec3(0, 66, 0))
    body.humanoid.walkSpeed = 6
end)

Line by line:

  • world:add("Part", { ... }) creates a part and parents it to the world. size is in metres, cframe places its centre at x 0, y 64, z 0, and anchored = true keeps physics from moving it, which is what you want for a floor.
  • game.players.joined fires once for each player who connects. It exists on the server only, because the server is the side that knows about everyone.
  • player:spawn(position) gives that player a body, puts it where you asked and hands it back. Two metres above the floor is enough to land on it.
  • body is that body, the same one player.character returns later. Its humanoid decides how it moves, so setting walkSpeed = 6 makes that player walk six metres a second.

Write the client script#

client/main.luau runs inside each player's own game. It sets the camera, reads input and draws interface for that player alone:

luau
camera.fov = 80

game.renderStepped:connect(function()
    camera.mode = if input:down("sneak") then "thirdPerson" else "firstPerson"
    camera.distance = 6
end)

camera and input are globals that exist on the client only. renderStepped runs once a frame, just before the frame is drawn, so the camera you set inside it is the camera the world is drawn against.

That is a whole place. There is no registration step and nothing to declare: a file that exists is a file that runs.

Press Play at the top of the editor, or F6, and you land on the floor in first person. Hold sneak to see yourself from behind. F6 again, or Right Shift, goes back to editing.

Each Play starts the place the way a player joining it does, so what you test is what players get. Client scripts run once the client has the world the server built for play, and they never see an instance that only exists in the editor.

Change something while it runs#

Save any file in the place and Moud restarts it. Everything the place created is destroyed and made again, which means you can change a number, save, and see the result without leaving the game.

It also means a variable that holds an instance from before the reload points at something that no longer exists:

luau
-- wrong: points at an instance the reload destroyed
local statue = game.world:find("statue")

-- right: looked up when it is needed
local function statue()
    return game.world:find("statue")
end

Every variable in your scripts is cleared by a reload. Anything that has to survive one goes in game.persist, a plain table the engine hands back after each restart:

luau
game.persist.joins = (game.persist.joins or 0) + 1

The two clocks#

Each side has its own loop:

luau
-- the server, once a tick. delta is seconds
game.stepped:connect(function(delta) end)

-- the client, once a frame. delta is seconds
game.renderStepped:connect(function(delta) end)

Write game state on stepped, where every player gets the same answer. Write the camera and anything that has to look smooth on renderStepped, which runs often enough to follow the frame rate.

Waiting without blocking#

task lets a script wait without stopping the tick that everyone else is on. Added to the server script, this makes a lantern next to the spawn blink:

luau
local lantern = world:add("Part", {
    name = "lantern",
    size = vec3(1, 1, 1),
    cframe = cframe(0, 66, -4),
    anchored = true,
})

task.spawn(function()
    while true do
        lantern.color = color(1, 0.88, 0.5)
        task.wait(0.6)
        lantern.color = color(0.3, 0.25, 0.2)
        task.wait(0.6)
    end
end)

task.delay(3, function() end)

local handle = task.spawn(function() end)
task.cancel(handle)

task.spawn starts a piece of work that can yield, task.wait yields for a number of seconds, task.delay runs something once later, and task.cancel stops work you started.

A loop without a task.wait inside it never gives the tick back, and the server stops answering anybody. If you write while true do, put a wait in it.

When something goes wrong#

A value an enum does not accept is an error that names what was allowed, instead of quietly falling back to a default:

luau
camera.mode = "birdseye"
-- error: mode: unknown value 'birdseye', expected one of firstPerson, thirdPerson, scriptable

Errors show up in the editor's Output panel: press Right Shift to open the editor over the running game. They are also written to the game log, logs/latest.log. See Debugging for what else the editor tells you about the running place.

Next steps#