Skip to content

Entity Movement

Berke edited this page Oct 5, 2026 · 1 revision

04 - Entity Movement

Scripts for moving props, platforms, and characters around. Position is a Vector3 where x and z are the ground plane and y is height. Move something by reading transform.position, building a new Vector3, and assigning it back (or use transform:addPosition(...) for relative moves).

Two ways to move something:

  • Teleport-style (transform.position = ...): simple and precise, but ignores walls and colliders. Good for props, pickups, and scripted platforms.
  • Physics-style (rigidbody.velocity = ...): the entity collides with the world and is affected by gravity. Good for characters. The enemy examples in 12_enemy_ai.md use this when a Rigidbody is present.

Patrol between two points

Attach to: the moving Entity. Create two empty Entities as waypoints and assign them.

-- Scripts/Movement/Patrol.lua (entity script)
public Entity pointA = nil
public Entity pointB = nil
public number speed = 30
public number waitTime = 1.0

local transform
local goingToB = true
local waiting = 0.0

function Start()
    transform = entity.transform
end

function Update()
    if pointA == nil or pointB == nil then return end

    local dt = GameTime.deltaTime

    if waiting > 0 then
        waiting = waiting - dt
        return
    end

    local target = goingToB and pointB or pointA
    local goal = target.transform.position
    local p = transform.position

    local dx, dz = goal.x - p.x, goal.z - p.z
    local dist = math.sqrt(dx * dx + dz * dz)
    local step = speed * dt

    if dist <= step then
        -- Arrived: snap to the point, turn around, and pause.
        transform.position = Vector3(goal.x, p.y, goal.z)
        goingToB = not goingToB
        waiting = waitTime
    else
        transform.position = Vector3(p.x + dx / dist * step, p.y, p.z + dz / dist * step)
        transform:lookDirection(Vector3(dx, 0, dz))   -- face the way we're going
    end
end

Notes

  • Movement stays on the ground plane and keeps the entity's own y.
  • transform:lookDirection turns the entity's rotation to face a direction (left and right only, by default). Directional sprites use the rotation to pick which side to show.
  • Want a longer route? Replace the two points with a table of waypoints and an index.

Orbit

Attach to: the orbiting Entity.

-- Scripts/Movement/Orbit.lua (entity script)
public Entity center = nil
public number radius = 40
public number degreesPerSecond = 90

local transform
local angle = 0.0
local cx, cz = 0.0, 0.0

function Start()
    transform = entity.transform

    local p = transform.position

    if center ~= nil then
        -- Orbit the assigned Entity, starting at the angle we are already at around it.
        local c = center.transform.position
        cx, cz = c.x, c.z
        angle = math.atan(p.z - cz, p.x - cx)
    else
        -- No center: put it `radius` behind the start position so the entity starts on its orbit.
        cx, cz = p.x - radius, p.z
    end
end

function Update()
    angle = angle + mathT.DegToRad(degreesPerSecond) * GameTime.deltaTime

    local p = transform.position
    transform.position = Vector3(cx + math.cos(angle) * radius, p.y, cz + math.sin(angle) * radius)
end

Notes

  • With a center assigned, the entity picks up the orbit at whatever angle it starts at (its distance from the center is replaced by radius). With no center, the orbit circle is placed so the entity's starting position is on it, and it doesn't jump on the first frame.

Bob and spin

Attach to: a pickup, a floating item, a decoration. It should not have a Rigidbody, or gravity will fight the bobbing.

-- Scripts/Movement/BobAndSpin.lua (entity script)
public number bobHeight = 3
public number bobSpeed = 2
public number spinDegreesPerSecond = 120

local transform
local baseY = 0.0
local phase = 0.0
local yaw = 0.0

function Start()
    transform = entity.transform
    baseY = transform.position.y
    phase = mathT.RandomF(0, 6.28)   -- so a row of pickups doesn't bob in lockstep
end

function Update()
    local dt = GameTime.deltaTime

    phase = phase + bobSpeed * dt
    yaw = yaw + math.rad(spinDegreesPerSecond) * dt

    local p = transform.position
    transform.position = Vector3(p.x, baseY + math.sin(phase) * bobHeight, p.z)

    -- Rotation is a quaternion (x, y, z, w). Rotating about the Y axis by `yaw`:
    transform.rotation = Vector4(0, math.sin(yaw / 2), 0, math.cos(yaw / 2))
end

Notes

  • Billboard sprites always face the camera, so the spin only matters for entities that render with their rotation.

Smooth follow

Attach to: the follower.

Eases toward a target instead of snapping, using exponential smoothing so the feel is the same at any frame rate.

-- Scripts/Movement/Follow.lua (entity script)
public Entity target = nil
public number followSpeed = 4
public Vector3 offset = Vector3(0, 0, 0)

local transform

function Start()
    transform = entity.transform
end

function Update()
    if target == nil or not target.isValid then return end

    -- 1 - e^(-k*dt) is a frame-rate independent blend factor.
    local k = 1 - math.exp(-followSpeed * GameTime.deltaTime)

    local p = transform.position
    local t = target.transform.position

    transform.position = Vector3(
        mathT.Lerp(p.x, t.x + offset.x, k),
        mathT.Lerp(p.y, t.y + offset.y, k),
        mathT.Lerp(p.z, t.z + offset.z, k)
    )
end

Keyboard mover

Attach to: any prop you want to push around with the arrow keys.

-- Scripts/Movement/KeyboardMover.lua (entity script)
public number speed = 40

local transform

function Start()
    transform = entity.transform
end

function Update()
    local dx, dz = 0.0, 0.0

    if Input.GetKey(Key.Left) then dx = dx - 1 end
    if Input.GetKey(Key.Right) then dx = dx + 1 end
    if Input.GetKey(Key.Up) then dz = dz + 1 end
    if Input.GetKey(Key.Down) then dz = dz - 1 end

    if dx == 0 and dz == 0 then return end

    -- Normalise so diagonals aren't faster.
    local len = math.sqrt(dx * dx + dz * dz)
    local step = speed * GameTime.deltaTime

    transform:addPosition(Vector3(dx / len * step, 0, dz / len * step))
    transform:lookDirection(Vector3(dx, 0, dz))
end

Notes

  • Input.GetKey is true while held, GetKeyDown only on the frame it was pressed, and GetKeyUp only on the frame it was released. Keys come from the Key table: Key.A-Key.Z, Key.Alpha0-Key.Alpha9, Key.Space, Key.Escape, Key.Enter, Key.Tab, Key.Backspace, Key.Left/Right/Up/Down, Key.LShift/RShift, Key.LCtrl/RCtrl, Key.LAlt/RAlt.

Clone this wiki locally