Skip to content

Your first script

We are going to build a door. Click it, it swings open. Click it again, it closes. Then we will make it close by itself, and remember whether it was open after the world restarts.

Nothing here assumes you have programmed before. Type it out rather than pasting it — the point is to see which part does what.

Before you start

You need an object in your world that you own. A plain box is fine; that is what we will treat as the door.

1. Open the script editor

Select the object and open its Edit window, then the Scripts tab. An object has 8 script slots. Click an empty one to start a new script.

You can also edit in VS Code instead, with autocomplete and error checking — see Editing in VS Code. Everything below works the same either way.

2. Make it react to a click

Replace whatever is in the slot with this:

luau
--!strict
local self = require("self")

self:on_touch(function(player)
    print(player.name .. " clicked the door")
end)

Save. Leave edit mode and click the object.

The message appears in your script console. Nothing else happens yet, which is correct — we only asked it to talk.

Three things to notice:

  • --!strict on the first line turns on error checking. Keep it there. It catches typos before you save rather than after somebody walks into them.
  • local self = require("self") brings in the object's own API. self means the object this script is inside.
  • self:on_touch(function(player) ... end) does not run now. It hands the engine a function and says call this when someone clicks me. The script then finishes and waits.

print goes to you, not to the player. It writes to the script console of the object's owner. It is for finding out what your script is doing, not for talking to players — for that, see Menus and HUDs.

3. Actually open it

A door is a thing that turns. Add a variable that remembers whether it is open, and turn the object when it changes.

luau
--!strict
local self = require("self")

local open = false

self:on_touch(function(player)
    open = not open

    if open then
        self:rotate_to(0, 90, 0)
    else
        self:rotate_to(0, 0, 0)
    end
end)

open = not open flips false to true and back. rotate_to sets the orientation in degrees, as three numbers: sideways tilt, turn, and roll. Only the middle one matters for a door.

Save and click. The door snaps between the two positions.

4. Make it swing instead of snap

Snapping looks like a mistake. tween moves the object smoothly over a number of seconds, and the movement is run by the world — your script does not stay awake during it.

luau
--!strict
local self = require("self")

local open = false

self:on_touch(function(player)
    open = not open

    self:tween({
        rot = { x = 0, y = open and 90 or 0, z = 0 },
        seconds = 0.6,
        ease = "in_out",
    })
end)

open and 90 or 0 reads as 90 if open, otherwise 0. It is the short way to write the if from the previous step.

ease is the shape of the movement: "linear" is a constant speed, "in_out" starts slow, speeds up and slows down again — which is what a heavy door does.

5. Close it by itself

self:after(seconds, fn) runs something once, later.

luau
--!strict
local self = require("self")

local open = false

local function swing(to_open: boolean)
    open = to_open
    self:tween({
        rot = { x = 0, y = open and 90 or 0, z = 0 },
        seconds = 0.6,
        ease = "in_out",
    })
end

self:on_touch(function(player)
    if open then
        swing(false)
        return
    end

    swing(true)

    self:after(4, function()
        if open then
            swing(false)
        end
    end)
end)

Two things worth stealing from this:

  • The repeated movement became a function (swing). Writing it once means changing the timing in one place.
  • The timer checks open again before acting. Four seconds is long enough for someone to have closed the door by hand, and a timer that fires blindly would slam a door that is already shut.

6. Make it remember

Your variables — open included — are wiped whenever the script is saved again or the world restarts. That is deliberate: a fresh start is predictable.

What survives is self.store, a small memory that belongs to the object.

luau
--!strict
local self = require("self")

local open = self.store:get("open") == "yes"

local function swing(to_open: boolean)
    open = to_open
    self.store:set("open", open and "yes" or "no")
    self:tween({
        rot = { x = 0, y = open and 90 or 0, z = 0 },
        seconds = 0.6,
        ease = "in_out",
    })
end

-- Put the door back where it was before the restart, without animating.
self:rotate_to(0, open and 90 or 0, 0)

self:on_touch(function(player)
    swing(not open)
end)

The store only holds text. A number goes in with tostring(n) and comes back with tonumber(s); here we store the words "yes" and "no" because that is all we need.

self.store:get returns nil when nothing was ever saved, so the first line reads as open is true only if the stored value is exactly "yes" — which is false on a brand new door, correctly.

7. Check what refusals tell you

Most verbs answer in the same shape: the thing you asked for, or nil plus a reason in plain words.

luau
local ok, why = self.store:set("open", "yes")
if not ok then
    print("could not save: " .. why)
end

You do not have to check every call while you are experimenting. Check them when something silently does not happen — the reason is almost always waiting in the return value.

What you learned

PieceWhat it does
require("self")the object this script is in
self:on_touch(fn)run fn when someone clicks
self:rotate_to(x, y, z)face this way, now, in degrees
self:tween{...}move there smoothly, over seconds
self:after(s, fn)run fn once, later
self.storememory that survives restarts
print(...)tell yourself what happened

Next

Hungrit scripting documentation.