Your first script#
We are going to build a door. Click it, it swings open. Click it again, it closes. Then we 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 instead of pasting it. The point is to feel which part does what.
What you need#
One object in your world that belongs to you. A plain box is perfect. That box is going to be the door.
1. Open the script editor#
Select the object, open its Edit window, then the Scripts tab. An object has 8 script slots. Click an empty one and you have a new script.
You can also write in VS Code, with autocomplete and error checking. See Editing in VS Code. Everything below works exactly the same either way.
2. Make it notice a click#
Put this in the slot:
--!strictlocal self = require("self") self:on_touch(function(player) print(player.name .. " clicked the door")end)Save, leave edit mode, and click the box.
The message shows up in your script console. Nothing else happens, which is correct. We only asked it to talk.
Three things are going on:
--!strict on the first line turns on error checking. Leave it there forever. It catches typos when you save instead of when a player walks into them.
local self = require("self") brings in the object's own set of verbs. self means the object this script is inside.
self:on_touch(function(player) ... end) does not run now. It hands the world a function and says call this when somebody clicks me. Then the script finishes and waits.
print talks to you, not to players
It writes to the script console of whoever owns the object. It is how you find out what your script is doing. To actually say something to a player, see Menus and HUDs.
3. Actually open it#
A door is a thing that turns. We need a variable that remembers whether it is open, and we turn the box when that changes.
--!strictlocal 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) endend)open = not open flips false to true and back again.
rotate_to sets which way the object faces, in degrees, as three numbers: tilt, turn, roll. Only the middle one matters for a door.
Save and click. The door jumps between the two positions.
4. Make it swing instead of jump#
Jumping looks like a bug. tween moves the object smoothly over a number of seconds, and the world runs that movement for you. Your script does not have to stay awake during it.
--!strictlocal 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 of writing the if from the last step, and you will see it everywhere.
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.
--!strictlocal 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 habits worth stealing from this:
The repeated movement became a function called swing. Written once, changed in one place. If you later decide the door should take 0.9 seconds, you edit one number instead of three.
The timer checks open again before it acts. Four seconds is plenty of time for somebody 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 on purpose. A fresh start is predictable.
What survives is self.store, a small memory belonging to the object.
--!strictlocal 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, with no animation.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 out with tonumber(s). Here we store the words "yes" and "no", because that is all we need.
self.store:get hands back nil when nothing was ever saved, so the first line reads as open is true only if the saved value is exactly "yes". On a brand new door that is false, which is right.
7. Read a refusal#
Most verbs answer in the same shape: the thing you asked for, or nil plus a reason in plain words.
local ok, why = self.store:set("open", "yes")if not ok then print("could not save: " .. why)endYou do not have to check every call while you are experimenting. Check them the moment something quietly does not happen. The reason is almost always sitting right there in the second return value.
What you just learned#
| Piece | What it does |
|---|---|
require("self") | the object this script is in |
self:on_touch(fn) | run fn when somebody clicks |
self:rotate_to(x, y, z) | face this way, now, in degrees |
self:tween({...}) | go there smoothly, over seconds |
self:after(s, fn) | run fn once, later |
self.store | memory that survives a restart |
print(...) | tell yourself what happened |
That is seven verbs, and it is genuinely most of what a simple object needs.
Next#
- Luau in an hour is the language itself, from zero
- How scripts run explains what Save actually does
- Doors and switches has the finished versions, including a door driven by a switch on the other side of the room
Updated 04 September 2026