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 = } |
Tip
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#
--!strictlocal 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 = falselocal 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 endend)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#
--!strictlocal self = require("self") local A = { x = -6, y = 2, z = 0 }local B = { x = 6, y = 2, z = 0 }local TRAVEL = 4local 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) endend 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#
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:
self:tween({ rot = { x = 0, y = 360, z = 0 }, seconds = 8, ease = "linear" })A door on a hinge, using a part#
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#
--!strictlocal 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) endend 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#
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:
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#
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)Careful
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:
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 to 600 s |
| Coordinates | ±100 km |
| Velocity and push | up to 100 m/s |
| Effects per event | 8 |
See also#
Updated 04 September 2026