Appearance
Moving things
Platforms, lifts, spinning parts, patrols and anything else that travels.
Choosing how to move something
| You want | Use |
|---|---|
| there, now | self:move_to |
| there, smoothly | self:tween |
| turn a bit more, repeatedly | self:rotate_by |
| a shove, with physics | self:push |
| a constant speed, with physics | self:set_velocity |
| a path you already know at birth | world:spawn{ to =, seconds = } |
Prefer tween to a timer that nudges. The region runs the movement and nothing is sent per frame, so it is smooth for everyone regardless of their connection — and your script sleeps through it.
A lift
luau
--!strict
local self = require("self")
local BOTTOM = { x = 0, y = 0.5, z = 0 }
local TOP = { x = 0, y = 8.0, z = 0 }
local TRAVEL = 3
local up = false
local busy = false
self:on_touch(function()
if busy then return end
busy = true
up = not up
local t = self:tween({
to = up and TOP or BOTTOM,
seconds = TRAVEL,
ease = "in_out",
})
if t then
t:on_done(function() busy = false end)
else
busy = false
end
end)The busy flag matters. Without it, clicking during the journey starts a new tween that replaces the running one, and the lift changes its mind halfway with somebody standing on it.
A platform that goes back and forth forever
luau
--!strict
local self = require("self")
local A = { x = -6, y = 2, z = 0 }
local B = { x = 6, y = 2, z = 0 }
local TRAVEL = 4
local PAUSE = 1
local at_a = true
local function leg()
at_a = not at_a
local t = self:tween({
to = at_a and A or B,
seconds = TRAVEL,
ease = "in_out",
})
if t then
t:on_done(function()
self:after(PAUSE, leg)
end)
end
end
self:move_to(A.x, A.y, A.z)
leg()Chaining on on_done rather than running a repeating timer means the pause is measured from the arrival, so the two never drift apart.
Something that spins
luau
self:on_timer(0.05, function()
self:rotate_by(0, 6, 0)
end)rotate_by adds to the current orientation, so repeated calls accumulate cleanly. Repeated absolute angles do not — they can lock up when two axes line up.
For a constant spin you never change, one long tween is cheaper than a timer:
luau
self:tween({ rot = { x = 0, y = 360, z = 0 }, seconds = 8, ease = "linear" })A door on a hinge, using a part
luau
local leaf = self:part("leaf")
if leaf then
leaf:rotate_by(0, 90, 0)
endA part's coordinates are local to the group, so a hinged door built with its pivot at the hinge turns correctly wherever the group is standing.
A patrol between several points
luau
--!strict
local self = require("self")
local ROUTE = {
{ x = 0, y = 1, z = 0 },
{ x = 10, y = 1, z = 0 },
{ x = 10, y = 1, z = 10 },
{ x = 0, y = 1, z = 10 },
}
local i = 0
local function step()
i = (i % #ROUTE) + 1
local target = ROUTE[i]
self:look_at(target.x, target.y, target.z)
local t = self:tween({ to = target, seconds = 5, ease = "linear" })
if t then
t:on_done(step)
end
end
step()look_at keeps the object upright and only uses the horizontal direction, so a target above or below does not tip it over.
A conveyor
luau
self:set({ dynamic = true })
self:set_velocity(2, 0, 0)set_velocity needs a physics body. Without dynamic = true you get nil, "not dynamic".
A projectile
State the whole flight at birth rather than animating it:
luau
local world = require("world")
world:spawn({
model = "arrow",
pos = tip,
to = target,
seconds = 0.4,
collide = false,
})One message instead of about fifty, and a player on a slow connection sees the shot already in progress when it reaches them rather than starting at the muzzle a round trip late.
Moving something you spawned
luau
local world = require("world")
local id = world:spawn({ shape = "box", pos = start })
world:tween(id, { to = finish, seconds = 3 })
world:move(id, 0, 5, 0)
world:despawn(id)self:tween moves the object the script is on. Used on a turret it sends the turret flying at the player while its projectiles sit at the muzzle. self: is me; world: with an id is the thing I made.
Making a click feel instant
The player's own machine can play the movement immediately:
luau
self:on_touch({
predict = {
{ when = "up", set = { up = false },
tween = { to = BOTTOM, seconds = 3 } },
{ when = { "not", "up" }, set = { up = true },
tween = { to = TOP, seconds = 3 } },
},
server = function(p) print(p.name .. " called the lift") end,
})Only for movements that always happen — anything refusable would be seen and then snapped back.
Limits worth remembering
| Tween length | 0.05 – 600 s |
| Coordinates | ±100 km |
| Velocity and push | up to 100 m/s |
| Effects per event | 8 |
