self#
self is the object your script is inside. Everything an object can do to itself is here.
Use it to
- make it react when someone clicks it, walks into it or comes near
- move it, turn it or glide it smoothly
- change its colour, its light or its physics
- let people pick it up, carry it or sit on it
- play sounds, and remember things across restarts
- break it, and mend it again
local self = require("self")self also works without the require, as a ready-made global. The require is still worth writing: it is what gives you autocomplete and error checking in VS Code.
At a glance#
What you want done, and the verb that does it.
React to people
Move and turn it
Change how it looks
Hurt and mend it
Let people carry it
Let people sit on it
Its pieces and its memory
How answers come back#
Almost every verb here answers in one of two shapes.
A reading hands you the value and never fails:
local here = self:position() -- always a pointlocal who = self:held_by() -- a player id, or nil if nobodyAn action hands you true (or a useful value), or nil and a reason:
local ok, why = self:move_to(0, 5, 0)if not ok then print("could not move: " .. why)endThe reason is always plain text meant to be read. You will meet "permission", "too many commands", "not dynamic" and "not held" most often; the full list is in Limits and refusals.
Reacting#
on_touch#
self:on_touch(fn) → handle self:on_touch({ predict = ..., server = fn }) → handle
Someone clicked the object, outside edit mode.
self:on_touch(function(player) print(player.name .. " clicked me")end)The handler receives a Player.
Registering again replaces the previous handler. A script has one touch handler, not a list. In a group, clicking any piece bubbles up to the nearest ancestor that has a script.
The table form: reacting instantly#
A click normally travels to the server and back, which is a visible pause. The table form lets you also describe what the click looks like, so the player's own machine plays it immediately while the real change happens on the server.
self:on_touch({ predict = { { when = "open", set = { open = false }, tween = { rot = { x = 0, y = 0, z = 0 }, seconds = 0.6 } }, { when = { "not", "open" }, set = { open = true }, tween = { rot = { x = 0, y = 90, z = 0 }, seconds = 0.6 } }, }, server = function(player) print(player.name .. " used the door") end,})| Key | Meaning |
|---|---|
predict | up to 8 rules, tried top to bottom; the first whose when holds is the one that plays |
server | the real handler, for anything a description cannot express |
Each rule may carry:
| Key | Meaning |
|---|---|
when | a condition over the object's own fields. "open", { "not", "open" }, { "==", "mode", "stopped" }, { ">", "visits", 10 }, { "<", ... }. Omitted = always |
tween | to, rot, scale, seconds, ease, the same words as tween |
set | fields to write, the same way set does |
sound | a named sound, played to whoever clicked |
Careful
Do not predict something that can be refused. A locked door, a purchase, a raffle. If the server might say no, the player would see it happen and then snap back. Predict only what always succeeds.
on_contact#
self:on_contact(fn) → handle
A player's body started touching the object: stepping on it, walking into it, hitting their head on it. This is physical contact, not a click.
self:on_contact(function(player) print(player.name .. " stepped on the plate")end)Fires once per approach, not once per tick while they stand there. Pair it with on_contact_end to bracket "while touching".
Bubbles up through a group like on_touch. Last registration wins.
on_contact_end#
self:on_contact_end(fn) → handle
The body stopped touching. Also fires when the player leaves the world while still touching, so every on_contact gets its pair.
on_region#
self:on_region(opts) → true, or nil, reason
A sphere around the object. near fires the moment somebody comes inside it, far the moment they leave.
self:on_region({ radius = 8, near = function(player) print(player.name .. " arrived") end, far = function(player) print(player.name .. " left") end,})| Key | Type | Meaning |
|---|---|---|
radius | number | metres, clamped to 0.5 to 96. 0 (or omitted) removes the volume |
near | function | someone entered |
far | function | someone left |
At least one of near / far is required.
Notes that save you trouble:
- Both are edges, not states. They fire on the crossing, never repeatedly while someone stands inside.
- The sphere follows the object, so a moving platform brings its trigger.
- Leaving takes half a metre more than entering, so somebody standing exactly on the line does not flicker in and out.
- A player who disconnects inside still gets their
far. - Calling it again replaces the volume and re-announces whoever is already inside.
on_action#
self:on_action(name, fn) → handle, or nil, reason self:on_action(name, opts, fn) → handle, or nil, reason
Someone pressed a key the world declared. This is how a weapon fires, a vehicle accelerates, a torch lights.
self:on_action("fire", { key = "mouse1" }, function(player, shot) local hit = world:raycast({ from = shot.eye, dir = shot.dir, ignore_player = player.id, })end)The handler receives the player and a shot: where they were and where they were aiming at the exact instant the button went down.
Field of shot | Meaning |
|---|---|
pos | where their feet were |
eye | where their eyes were, for their stance. Cast from here |
dir | where the shot is going: a unit direction, with recoil and shot cone already applied |
down | true on the press, false on the release |
Tip
Always cast from shot.eye, never from shot.pos plus a number you chose. A number that is right standing up is wrong crouching, because the eye drops about 55 centimetres and your constant drops nothing, so the shot leaves half a metre above the crosshair.
Options#
| Key | Type | Meaning |
|---|---|---|
key | string | the suggested key: "mouse1", "mouse2", "r", "space", a letter, a digit |
release | function | runs when the button is released, for hold-to-aim, charge-and-release |
camera | table | what the camera does while held. See camera presets |
recoil | table | how firing kicks the view. See recoil |
auto | number | seconds between repeats while held. Omitted = one press, one event. Clamped to 0.05 to 5 |
Fires on the edge of the press. Holding the trigger for two seconds is one event, not a hundred and twenty, unless you set auto.
Careful
An action belongs to the world, not to your object. Two objects declaring "fire" are one trigger: the player has one finger, not one per object in the room. That is why a weapon's recoil and rate of fire belong on set_weapon, which is per player, and not on the action, which everybody shares.
Names may use letters, digits, _ and -. A world may declare up to 32 actions. The server never learns which key was pressed, only that the named action happened, because players can rebind freely.
on_hud#
self:on_hud(fn) → handle
A button on a menu this object showed reported back.
self:on_hud(function(player, event, data) if event == "buy" then print(player.name .. " bought " .. tostring(data.item)) endend)| Argument | Meaning |
|---|---|
player | who clicked. Carries show_hud / update_hud / hide_hud, so you can answer by changing the menu |
event | the name the button's emit used |
data | the fields that button sent |
Only buttons with emit reach here. Changing pages, moving a slider and ticking a box all happen on the player's own machine and never touch your script. See Menus and HUDs.
Last registration wins.
on_timer#
self:on_timer(seconds, fn) → timer handle
Runs fn every seconds, forever.
local beat = self:on_timer(1, function() self:rotate_by(0, 15, 0)end) -- laterbeat:cancel()seconds must be greater than 0. The handle has cancel(), which is safe to call twice.
Timers do not fire while the region is asleep. See how scripts run.
after#
self:after(seconds, fn) → timer handle
Runs fn once, later. A door that closes itself, a cooldown, a delayed explosion.
self:after(4, function() self:set({ light = false })end)seconds must be 0 or more. after(0, ...) runs on the next tick, which is a useful way to let the current event finish first.
cancel() before it fires disarms it; after it fired it does nothing.
Moving#
Coordinates are world coordinates when the object stands on its own, and local to the parent when it is inside a group. Angles are always degrees.
position#
self:position() → point
Where the object is. Never fails.
local here = self:position()print(here.x .. ", " .. here.y .. ", " .. here.z)The result is a vec: it has x, y, z, it has maths operators, and it can be handed straight to anything that takes a point.
move_to#
self:move_to(x, y, z) → true, or nil, reason self:move_to(point) → true, or nil, reason
Puts the object somewhere, at once.
self:move_to(0, 5, 0)self:move_to(vec(0, 5, 0))self:move_to(other_place)Coordinates are limited to 100 km from the origin.
rotation#
self:rotation() → point
Current orientation in degrees, as a vec. A single-axis round trip is exact; an orientation built from several axes may come back as an equivalent trio of angles rather than the exact numbers you set.
rotate_to#
self:rotate_to(x, y, z) → true, or nil, reason
Absolute orientation in degrees. y is the one that turns an object on the spot.
self:rotate_to(0, 90, 0) -- face a quarter turn roundrotate_by#
self:rotate_by(x, y, z) → true, or nil, reason
Relative turn, from wherever it is pointing now. This is what a hinge, a wheel or a spinning coin wants. Repeated rotate_by calls add up cleanly and never lock up the way repeated absolute angles can.
self:on_timer(0.05, function() self:rotate_by(0, 6, 0) -- spins foreverend)look_at#
self:look_at(x, y, z) → true, or nil, reason
Turns to face a point, staying upright: the object's forward side ends up pointing at the target, and only the horizontal direction is used, so looking at something overhead turns without tipping over.
local target = players:get(id)if target then self:look_at(target.pos)endAnswers nil, "look_at: target is not a direction" when the target is straight up, straight down, or the object's own position.
scale#
self:scale() → point
Current size per axis, as a vec.
scale_to#
self:scale_to(n) → true, or nil, reason self:scale_to(x, y, z) → true, or nil, reason
One number resizes uniformly; three resize per axis. Clamped to 0.01 to 10000.
self:scale_to(2) -- twice as bigself:scale_to(1, 3, 1) -- three times tallertween#
self:tween(opts) → tween handle, or nil, reason
Moves, turns or resizes smoothly over time. The world runs the movement, so your script does not stay awake during it, and nothing is sent per frame.
self:tween({ to = { x = 0, y = 5, z = 0 }, rot = { x = 0, y = 180, z = 0 }, seconds = 2, ease = "in_out",})| Key | Type | Meaning |
|---|---|---|
to | point | final position |
rot | point | final absolute orientation, degrees |
scale | number or point | final size |
seconds | number | required, clamped to 0.05 to 600 |
ease | string | "linear" (default), "in", "out", "in_out" |
At least one of to / rot / scale is required.
The handle carries:
local t = self:tween({ to = up, seconds = 1 }) t:on_done(function() print("arrived")end) t:cancel() -- stop where it is; on_done does NOT runStarting a new tween replaces the running one, and an explicit move_to, rotate_to or scale_to cancels it. Only the active tween's handle responds; an old one becomes a silent no-op.
Physics#
Both verbs need the object to be a physics body, set with self:set({ dynamic = true }). Otherwise they answer nil, "not dynamic".
push#
self:push(x, y, z) → true, or nil, reason
An instant shove: adds to the object's velocity, in metres per second.
self:push(0, 8, 0) -- a hopMass-independent on purpose. The same push moves a small crate and a big one equally, so you can tune a jump without also tuning every object's weight. Magnitude is clamped to 100 m/s.
set_velocity#
self:set_velocity(x, y, z) → true, or nil, reason
Sets the velocity outright, rather than adding to it. A conveyor, a cannonball, or set_velocity(0, 0, 0) to stop something dead. Same 100 m/s clamp.
velocity#
self:velocity() → point
How fast the object is moving right now, in metres per second. Only a dynamic object has a speed of its own; anything else reads zero. The reading is as fresh as position: the tick's, and zero on the very first call of a world's life, before the region has filled it.
-- glow while it is going fastself:on_timer(0.2, function() local fast = self:velocity():len() > 12 self:set({ emissive = fast and 2 or 0 })end)Another object's is obj:velocity().
Appearance#
set#
self:set(props) → true, or nil, reason
Writes properties, using the same vocabulary as the Edit window. One call is one change, however many keys it carries, so batch them.
self:set({ color = "#55e38b", emissive = 1.5, light = true, light_radius = 12,})Up to 32 keys per call. The complete list of what you can write, meaning colours, lights, physics, per-face painting and health, is in Object properties.
Colours accept "#rrggbb", "#rrggbbaa", a colour name, or { r, g, b }.
get#
self:get(key) → value, or nil
Reads one property back. A property that was never written reads as nil, which means "the engine's default", not "zero".
if self:get("dynamic") then self:push(0, 6, 0)endparticles#
self:particles(opts) → true, or nil, reason self:particles() → removes the emitter
Attaches fire, smoke, sparks or dust to the object. The emitter belongs to the object: it follows it and dies with it. Nothing is sent per particle, so every viewer draws the same field from the description, so it costs the same for one watcher or forty.
self:particles({ preset = "fire" }) -- a whole campfireself:particles({ preset = "smoke", rate = 40, glow = 0 })Start from a preset and override only what you care about. The full option table, all thirty-odd fields of it, is in Particles.
Damage#
An object is damageable only if it has a max_health. Set it in the Edit window or with self:set({ max_health = 100 }). Without it, both verbs answer nil, "this object has no max_health — it cannot be damaged".
That is deliberate: it is how a world says which props are destructible and which are scenery.
damage#
self:damage(amount) → remaining health, or nil, reason self:damage(amount, { by = uuid }) → remaining health, or nil, reason
local left = self:damage(25, { by = player.id })if left and left <= 0 then print("felled")end| Key | Meaning |
|---|---|
by | the player id to credit, for a scoreboard or a kill feed |
Tip
The number you get back is a prediction. The subtraction itself happens on the server, because two axes hitting the same tree on the same tick would otherwise both read the same health and the tree would take one hit for two swings. What you receive is what your blow would leave, which is exactly what you need to decide whether you just felled it.
For the authoritative outcome, listen for world.destroyed. See Damage and destruction.
heal#
self:heal(amount) → remaining health, or nil, reason
The same effect, negated. Same rules, same prediction.
Being carried#
hold#
self:hold(opts) → true, or nil, reason
Puts this object into a player's hand.
self:on_touch(function(p) self:hold({ player = p.id, at = "right_hand" })end)| Key | Type | Meaning |
|---|---|---|
player | string | required. The player id |
at | string | which socket. "right_hand" (default), "left_hand", "back", "head", "waist", "chest" and the rest. See Sockets |
pos | point | where it sits on the socket, in metres |
rot | point | how it is turned on the socket, in degrees |
scale | number or point | multiplies the size it already has |
smooth | number | seconds to slide there from wherever it was. 0 snaps |
settle | boolean | declares the landing for the whole carry. See below |
fp | table | a second grip, for the carrier's own first-person view. See below |
Calling hold again with a different at moves it between sockets. That is how a weapon comes off the back and into the hand; there is no second verb.
The axes are the same on every socket#
+x is right, +y is up, −z is forward, relative to the body. The hand, the back and the first-person eye all agree, so a number you found for one is a real starting point for another.
Each socket still needs its own pose, because something lies across a back differently from how it sits in a fist. Find the numbers with the grip window (</> → Pegada), which lists everything you are carrying and hands back a table to paste straight into this call.
The first-person grip#
The person holding it does not see it where everybody else does, and that is not a bug to tune away: in third person the object hangs off the hand bone of a body you can see, and in first person that body is not drawn at all, so it hangs off the hands in front of the eye instead. Two different frames, and one set of numbers cannot be right in both. Tune the grip until the rifle sits well in everyone else's view and it is through your own chin; tune it for your own view and it is through everyone else's wrist.
fp is the second answer. Same three keys, same axes, same degrees:
self:hold({ player = p.id, at = "right_hand", -- what everybody sees pos = { x = 0.02, y = -0.01, z = 0.06 }, rot = { x = 0, y = 90, z = 0 }, -- …and what YOU see, holding it fp = { pos = { x = 0.14, y = -0.18, z = -0.35 }, rot = { x = -2, y = 88, z = 4 }, scale = 1, },})Leave fp out and first person keeps using the grip above, which is what every world that has never mentioned it already does. It changes nothing for anybody else: every other player draws the shared grip off the hand bone, whatever is in here.
It only applies where a viewmodel applies: your own screen, in first person, in a hand. An object on your back is not drawn to you at all, and an object in somebody else's hand is never yours to see this way.
What fp is measured from. If the player has first-person arms (set_viewmodel) and that model carries a skeleton, fp hangs off the model's own hand bone: move, turn or rescale the arms in the Braços tab and the weapon goes with them, and a gesture that pulls the hand down takes the gun along. Zero rotation means aligned with the arms, whatever twist the rigger left in that bone.
Without arms, or with arms that have no skeleton, there is no hand to hang from, so fp is measured from the eye, exactly as before.
Tune the arms first and the weapon second: the arms decide where the hand is, and the grip is where the weapon sits in it.
settle#
settle = true says: be physical while it falls, ordinary geometry once it stops, however the carry ends.
Prefer it here rather than on drop. Three of the four ways a carry can end never call drop at all. The player pressing the put-down key, walking out of the region, or losing their connection all end it, so a settle given only there silently does nothing on those, and an object that was already a physics body is left dynamic forever.
Refusals#
| Reason | Meaning |
|---|---|
"permission" | that player has not interacted with this object. A touch, or a button on a HUD you showed them, counts |
"socket" | no attachment point by that name |
"child" | hold the root of a group, never one of its pieces |
"held" | somebody else has it. Taking it from their hands is a rule you write, never a default |
"full" | their hands are full |
Careful
The object stays in this region. It does not go into anybody's inventory and it does not follow them anywhere. Walk out carrying it and the region puts it down where you were standing, and your on_drop hears about it.
drop#
self:drop(opts) → true, or nil, "not held"
Lets go. The object is released in front of the carrier at chest height, and physics takes it from there.
self:drop({ settle = true }) self:drop({ throw = { x = look.x * 4, y = 2, z = look.z * 4 }, settle = true,})| Key | Type | Meaning |
|---|---|---|
throw | point | velocity on the way out, m/s |
physics | boolean | see the table below |
settle | boolean | physical only while it falls |
| What you pass | What happens |
|---|---|
physics = true | falls and tumbles, and stays a body you can shove around forever |
physics = false | stands on the ground and stays put |
| neither | whatever it was before it was picked up |
settle = true | drops like physics = true, then goes back to being static the moment it stops |
settle is what most props want, and neither of the other two gives it: the default puts the object down without it ever falling, so a drop over a ledge lands on air. You do not need to pair it with physics = true. Asking to settle is asking for the fall.
The region decides where it is released, never where it ends up.
held_by#
self:held_by() → player id, or nil
Who is carrying it. Never fails.
held_at#
self:held_at() → socket name, or nil
Which socket it is on: "right_hand", "back", and so on.
Tip
Ask the engine every time, and never keep your own copy. An object can leave a hand by routes your script never runs: the player pressing the put-down key, walking out of the region, losing their connection. A local variable saying "I am held" would go on saying it about an object lying on the floor.
local function in_hand(): boolean return self:held_at() == "right_hand"endon_hold#
self:on_hold(fn) → handle
Somebody picked this object up.
Fires for every hold, including one that moves the object to another socket. If you need "is it actually in a hand now", ask held_at() inside the handler rather than assuming.
on_drop#
self:on_drop(fn) → handle
It left somebody's hands. The handler receives the player and a reason:
| Reason | Meaning |
|---|---|
"script" | you called self:drop |
"released" | the carrier put it down themselves. Always available to them, and no script can refuse it |
"left" | they are no longer in this region. Includes a lost connection |
"gone" | the object itself was deleted while held |
Register this if the object matters to your game. Three of the four are not something you asked for, and they are what keeps your bookkeeping honest.
Being sat on#
The mirror of carrying: hold puts the object on the person, sit puts the person on the object.
self:on_touch(function(p) self:sit(p)end)That is a whole chair. Whoever clicks it sits; walking or jumping gets them up.
A chair that only needs this needs no script at all: switch on Seat in the editor's Physics page. On a part of a group, that part is the seat. A script on the same object still hears on_sit and on_stand.
The Seat settings are where the seat is (X, Y, Z), which way the sitter faces, whether a click sits, and what gets them up. They apply live: sit on it, then drag the sliders and watch your own avatar move. A script's self:sit uses them for every option it leaves out.
-- Seat settings: "Sit on click" OFF, so only this prompt seats anybody.local PROMPT = { id = "sit", world = { height = 1.0, distance = 3, gaze = 25 }, size = { 100, 100 }, vars = { t = 0 }, pages = { main = { { "ring", var = "t", size = 30, thickness = 2 }, { "text", "Hold E to sit", size = 10, align = "center" }, { "hold", key = "e", dur = 0.8, var = "t", emit = "sit" }, } },} self:on_region({ radius = 3, near = function(p) p:show_hud(PROMPT) end, far = function(p) p:hide_hud("sit") end,}) self:on_hud(function(p, event) if event == "sit" then p:hide_hud("sit") self:sit(p) endend)sit#
self:sit(player, options?) → true, or nil, reason
Pins that player to this object. If the object moves, they move with it.
player is the one a handler gave you (p), or their id.
| Option | Meaning |
|---|---|
pos | where on the object they sit, in the object's own space. vec(0, 0.45, 0) is 45 cm above its origin |
yaw | which way they face, in degrees about the object's up. 0 faces the object's −Z |
exit | what gets them up: "move" (walk or jump), "jump" (only jump; the walk keys do nothing), "locked" (only a script) |
Each one left out comes from the object's Seat settings, or else the default: the origin, 0, "move".
local vec = require("vec")local pose self:on_touch(function(p) local ok, why = self:sit(p, { pos = vec(0, 0.45, 0) }) if ok then pose = p:play_animation("sitting") -- an animation in this object's Contents endend) self:on_stand(function() if pose then pose:stop() end pose = nilend)Sitting does not pose anybody. It decides where the body is; what the body looks like is play_animation, as above.
| Reason | Meaning |
|---|---|
permission | they did not click this object, and the script's owner may not act on other people here |
occupied | somebody else is sitting on this object |
player is required | the first argument was not a player |
pos is too far from the object | a seat is on the thing. At most 8 m from its origin |
yaw must be a number of degrees | yaw was not a number |
exit must be move, jump or locked | exit was another word |
It can also come to nothing after answering true: the player has to be within 6 m of the seat when it happens, or sitting would be a teleport. Check seated or wait for on_sit if it matters.
Who may be sat
The click is the consent. A player who clicked this object, or pressed a button on a HUD it showed them, can be sat by it in any world, with no permission from anybody. Sitting somebody who did not interact is acting on their avatar and needs Act on other people's avatars.
The way out is always theirs. exit makes leaving deliberate, never impossible: holding jump for 2 seconds stands anybody up from any seat, even a "locked" one. The player is told how to get up when they sit.
One object seats one person. The seat is the object this script is in; if that is a part of a group, the seat moves with the whole group.
stand#
self:stand(player?) → true, or nil, "not seated here"
Gets up whoever is sitting on this object.
seated#
self:seated() → the id of whoever is sitting here, or nil
on_sit#
self:on_sit(function(player) … end) → handle
Somebody is now sitting on this object.
on_stand#
self:on_stand(function(player, why) … end) → handle
why | Meaning |
|---|---|
"stood" | they walked or jumped |
"script" | a script called stand, or sat them somewhere else |
"gone" | this object's root was deleted |
When somebody leaves the region while sitting, the seat is simply free again and this is not called: there is nobody left to hand you.
Sound#
The name of a sound is either an item in this object's Contents, a path on the world's shared library (world/sounds/thunder), or one of the built-in cues that need no upload.
Tip
The slash is the whole difference. A bare name lives inside this object and travels with it. Take it, sell it, drop it in another world, and the sound is still there. A world/… name lives in the world you are standing in: one copy fifty objects can share, swappable in one place, and nothing an object carries away with it.
play_sound#
self:play_sound(name) → true, or nil, reason self:play_sound(name, opts) → true, or nil, reason
Makes a noise at this object, heard by whoever is near enough. It follows the object.
self:play_sound("hammer")self:play_sound("hammer", { volume = 0.9, pitch = { 0.95, 1.05 } })| Key | Type | Meaning |
|---|---|---|
volume | number | 0 to 4. 1 is the level it was recorded at |
pitch | number or {low, high} | 0.25 to 4. The pair picks one value per play |
range | number | metres past which nobody hears it |
near | number | metres inside which it does not get any quieter |
falloff | string | "inverse" (how the real world works) or "linear" (a zone with a definite edge) |
air | number | 0 to 1, how much of the top end distance eats |
distant | string | another sound in this object that takes over further out |
fade | number | seconds to fade in. 0 is a cut |
loop | boolean | keep playing, but prefer sound |
channel | string | names the emitter, so one object can run several |
Three of these are worth understanding rather than copying:
pitchas a pair is what stops four identical samples a second from sounding like a machine. The region picks the value, so everybody in earshot hears the same one.nearmatters more than it looks: without it, a sound you are standing on is infinitely loud.distantis how far-away things really work. Something 300 m away is not its own close-up sound turned down; it is a different recording.
range is also the radius the server uses to decide who is even told about the sound, so it is a cost as well as a choice.
sound#
self:sound(name, opts) → true, or nil, reason
The same thing with the loop already on: keep making this noise. A motor, a fire, a radio.
self:sound("motor", { volume = 0.4, fade = 1.5, channel = "motor" })It plays until stop_sound, follows the object, and is heard by people who arrive later, because a looping emitter is a property of the object, not an event that happened once.
Takes the same options as play_sound, minus loop.
channel names the emitter so one object can run more than one. Using the same channel twice replaces rather than stacking, which is what makes a script safe to re-run.
stop_sound#
self:stop_sound() → true, or nil, reason self:stop_sound(channel, fade) → true, or nil, reason
Ends a looping emitter. With no channel it stops the one self:sound started without naming one, the only emitter most objects have.
fade defaults to a short ramp rather than to zero, because a motor that stops dead sounds like a bug and a cut in a waveform is an audible click.
Pieces and memory#
part#
self:part(name) → part, or nil
One direct child of this object, by name. Repeated names give you the first one found.
local lid = self:part("lid")if lid then lid:rotate_to(0, 0, -80)endA part has its own small API: set, get, move_to, position, rotate_to, rotate_by, rotation, scale_to, scale. Its coordinates are local to the group. A part that has been deleted answers nil, "unknown part".
parts#
self:parts() → list of parts
Every direct child.
for _, p in ipairs(self:parts()) do p:set({ color = "#883322" })endstore#
self.store is the object's own memory, which survives a restart.
self.store:set("visits", "1")local v = self.store:get("visits") -- "1", or nilself.store:set("visits", nil) -- removes the keyBoth are fallible: get and set answer the value (or true) plus a reason on failure.
Values are always text. Convert on the way in and out:
local n = tonumber(self.store:get("coins") or "0") or 0n += 10self.store:set("coins", tostring(n))The ceilings, and what happens at them:
| object | personal HUD | |
|---|---|---|
| key length | 256 B | 256 B |
| value size | 64 KB | 8 KB |
| number of keys | 256 | 64 |
| total | 1 MB | 32 KB |
A full store still reads, overwrites and deletes. Only a brand new key is refused, and nothing of yours is erased behind your back. set answers nil, "store full" or nil, "value too large" rather than failing silently.
contents#
self.contents is what the creator put inside this object.
local item, why = self.contents:get("arrow")if item then world:spawn({ model = "arrow", pos = tip })end local n = self.contents:count()for _, c in ipairs(self.contents:list() or {}) do print(c.name .. " (" .. c.kind .. ")")end| Verb | Answers |
|---|---|
list() | every item, in the order the creator filled it |
get(name) | one item, or nil with no reason. Absent is an answer, not a failure |
count() | how many, without building the list |
Each item has a name and a kind ("mesh", "texture", "animation", "script", "sound", "object").
Read only. Putting something in is an act of a person with an inventory; a script has none of its own. What a script does with contents is use them, pass the name to world:spawn, self:play_sound, and so on.
There is no asset field and there never will be: a script refers to what the object holds by name, and the server resolves it when it is used. A name is what you wrote and what still reads correctly a year later.
Empty and harmless on a HUD or a worn item, which have no object to hold anything.
owner#
self:owner() → player id
Who owns this object. Never fails.
self:on_touch(function(p) if p.id ~= self:owner() then print("only the owner may use this") return endend)Worn items only#
A script on an item somebody wears gets a slightly different self: it has its own transform relative to the bone, memory that crosses worlds, and these three events. It has no push, set_velocity, on_touch or on_contact, because a worn item is not in the world's object tree and has no collider.
on_attach#
self:on_attach(fn) → handle
The item was put on, or this world just started the script.
on_detach#
self:on_detach(fn) → handle
The item was taken off, or this world is releasing the script. It runs before the script is torn down, which makes it the last moment you can write to self.store.
on_world_enter#
self:on_world_enter(fn) → handle
New world, old memory. A worn item's script is torn down on every world switch and started again on the other side; only self.store survives. This event names that instant.
See also#
- world is the region: spawning, raycasts, weather, time
- players is who is here, and what they are doing
- Player and screen covers HUDs, crosshair, camera, weapons
- Object properties lists every key
setaccepts - Limits and refusals has every ceiling and every reason
Updated 17 October 2026