Hungrit Docs

Luau in an hour#

The language scripts are written in is called Luau. This page teaches enough of it to build real things, and it assumes you have never programmed before.

Read it once, build something, then come back. Nobody remembers a language from one pass.

If you have written JavaScript, Python or Lua, skim it and stop at the boxes marked worth knowing. Those are the parts that differ.

Values#

A value is a piece of information. There are four kinds you will use.

Luau
local name = "Mira"        -- text, called a stringlocal coins = 40           -- a numberlocal locked = true        -- true or false, called a booleanlocal nothing = nil        -- "there is nothing here"

local means this belongs to me. Always write it. A variable without local is visible to your whole script, and that is a reliable source of confusing bugs.

Numbers are just numbers. There is no separate whole number type. 3 and 3.5 are the same kind of thing.

nil is not zero and it is not false. It means absent. Reading something that was never set gives you nil, which is why so many examples check for it.

Doing things with values#

Luau
local coins = 40local total = 10 + 5        -- 15local half = 10 / 4         -- 2.5local rest = 10 % 3         -- 1, the remainder local greeting = "Hello, " .. "Mira"print(greeting, total, half, rest, coins)

Joining text uses two dots, not +. Adding a number to text is an error, so convert it first:

Luau
local coins = 40print("You have " .. tostring(coins) .. " coins")

Shorthands you will see everywhere:

Luau
local coins = 40coins += 1     -- same as coins = coins + 1coins -= 5coins *= 2

Making decisions#

Luau
local coins = 40if coins >= 100 then    print("rich")elseif coins >= 10 then    print("getting there")else    print("broke")end

The comparisons are == (equal), ~= (not equal, not !=), <, >, <= and >=.

Combining them:

Luau
if locked and coins >= 10 then ... endif locked or coins >= 10 then ... endif not locked then ... end

Worth knowing

Only false and nil count as "no". Zero is true in a condition, and so is an empty string. if coins then is true even when coins is 0, so write if coins > 0 then when that is what you actually mean.

The short form#

This turns up constantly in real scripts:

Luau
local open = truelocal angle = open and 90 or 0print(angle)

Read it as 90 if open, otherwise 0. It does the same job as the four line if, and it is safe as long as the middle value is never false or nil.

Repeating things#

Luau
for i = 1, 5 do    print(i)          -- 1, 2, 3, 4, 5end for i = 10, 0, -2 do    print(i)          -- 10, 8, 6, 4, 2, 0end

Never write a loop with no end

while true do ... end burns through your script's instruction budget and the script gets killed. It stops running until you save it again.

Anything that should happen over and over in time is a timer, not a loop:

Luau
local self = require("self") self:on_timer(1, function()    -- once a second, forever, and free in betweenend)

Functions#

A function is a named piece of behaviour you can run more than once.

Luau
local function greet(who: string)    print("Hello, " .. who)end greet("Mira")greet("Ash")

They can hand a value back:

Luau
local function double(n: number): number    return n * 2end local x = double(21)   -- 42print(x)

Functions handed to other functions#

This is the shape every event uses, so it is worth a moment.

Luau
local self = require("self") self:on_touch(function(player)    print(player.name)end)

function(player) ... end with no name is a function being handed to on_touch, which keeps it and calls it later. Written out the long way, it is the same thing:

Luau
local self = require("self") local function whenTouched(player)    print(player.name)end self:on_touch(whenTouched)

Both are fine. The short one is what you will mostly see.

Tables#

A table holds several values at once. It is the only container Luau has, and it does two different jobs.

As a list#

Luau
local colors = { "red", "green", "blue" } print(colors[1])    -- "red". Counting starts at 1, not 0print(#colors)      -- 3. The # means "how many" table.insert(colors, "yellow")table.remove(colors, 1)        -- takes out "red" for i, c in ipairs(colors) do    print(i .. ": " .. c)end

As a set of named fields#

Luau
local door = {    open = false,    speed = 0.6,} print(door.open)      -- falsedoor.open = true

This second form is what every option in this API uses:

Luau
local self = require("self") self:tween({ rot = { x = 0, y = 90, z = 0 }, seconds = 0.6, ease = "in_out" })

Worth knowing

When a function takes exactly one table, you may drop the parentheses. These two lines are identical:

Luau
local self = require("self")local here = self:position() self:tween({ seconds = 1, to = here })self:tween{ seconds = 1, to = here }

Both spellings appear in these docs. Neither is more correct than the other.

Dots and colons#

This one trips up everybody exactly once.

Luau
self:move_to(0, 5, 0)     -- colon: telling self to DO somethingself.store                -- dot: reading a value OUT of self

Use : when something should act. Use . when you want a value. If you ever see attempt to call a nil value, a swapped dot and colon is the first thing to check.

Types, which are optional and worth it#

The --!strict on line 1 turns on checking. After that you can say what kind of value something is:

Luau
--!strict local function damage(amount: number, target: string): boolean    print(target)    return amount > 0end

You never have to annotate anything. What --!strict buys you is that your editor catches typos and wrong arguments before you save, instead of you finding them by clicking around the world wondering why nothing happens.

The one habit worth forming#

Many calls answer with a value or nil plus a reason. Reading a field out of nil is far and away the most common script error.

Luau
local self = require("self") local part = self:part("hinge")if not part then    print("no part called 'hinge'")    returnend part:rotate_by(0, 5, 0)     -- safe, because we know it is there

The pattern is always the same. Check, leave early, then carry on. It keeps the interesting code out of a nest of ifs.

Two shorthands for defaults:

Luau
local self = require("self") local n = tonumber(self.store:get("count") or "0") or 0print(n)

a or b means a, unless it is nil or false, in which case b. Here it survives both a missing saved value and saved text that is not a number.

Built-ins you will actually use#

Luau
math.floor(3.7)          -- 3math.ceil(3.2)           -- 4math.abs(-5)             -- 5math.min(3, 9)           -- 3math.max(3, 9)           -- 9math.random(1, 6)        -- a whole number from 1 to 6math.random()            -- a fraction from 0 to 1 tostring(42)             -- "42"tonumber("42")           -- 42tonumber("banana")       -- nil string.upper("hi")       -- "HI"string.format("%.1f", 3.14159)   -- "3.1" #"hello"                 -- 5, the length of the text

Comments#

Luau
-- a note to yourself. The engine skips it. --[[    several lines]]

Putting it together#

A lamp that starts off, comes on when somebody walks up, and switches itself off a few seconds after the last person leaves:

Luau
--!strictlocal self = require("self") local inside = 0local offTimer = nil local function setLight(on: boolean)    self:set({        light = on,        emissive = on and 2 or 0,        color = on and "#ffe9b0" or "#3a3a3a",    })end setLight(false) self:on_region({    radius = 6,     near = function(player)        inside += 1         if offTimer then            offTimer:cancel()            offTimer = nil        end         setLight(true)    end,     far = function(player)        inside -= 1        if inside > 0 then            return        end         offTimer = self:after(4, function()            offTimer = nil            setLight(false)        end)    end,})

Every idea in it is on this page: variables, a function, if, the short form, a table of options, and functions handed to other functions. The counter exists because two people can be standing in the circle, and the light should only go out when the second one leaves.

Next#

Updated 04 September 2026