Skip to content

Moving things

Platforms, lifts, spinning parts, patrols and anything else that travels.

Choosing how to move something

You wantUse
there, nowself:move_to
there, smoothlyself:tween
turn a bit more, repeatedlyself:rotate_by
a shove, with physicsself:push
a constant speed, with physicsself:set_velocity
a path you already know at birthworld: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)
end

A 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 length0.05 – 600 s
Coordinates±100 km
Velocity and pushup to 100 m/s
Effects per event8

See also

Hungrit scripting documentation.