Lua ScriptingIntermediate

Cross-object interactions

Find nearby objects, destroy or knock them back, inspect their transforms, handle triggers, and coordinate state between scripts.

Find another object at runtime

Object scripts can look up scene objects by UUID, exact name, name prefix, or object type. Spatial queries return UUIDs for overlapping physics bodies; resolve each UUID with gameObject.getObject(id) before reading its transform or calling methods on it.

MethodResult
getObject(uuid)One object API or nil
getObjectByName(name)The first exact-name match or nil
getObjectsByNamePrefix(prefix)An array of object APIs
getObjectsByType(type)An array of object APIs
sphereQuery(radius)UUIDs of physics bodies overlapping a sphere centered on this object
boxQuery(halfExtents)UUIDs of physics bodies overlapping a box centered on this object

Filter spatial results

Spatial queries return physics-body UUIDs, not names, types, or tags. Build a set from a name-prefix lookup when only a particular group should react.

Characters count as physics bodies here. Raycasts and sphere and box queries all find a character through a hitbox matching its standing or crouched capsule, reported under the character's own UUID, so a player's hitscan can hit an NPC character directly. A query a character casts from its own position starts inside its own hitbox, so skip results equal to gameObject.id.

Destroy an object

Every resolved object API — including gameObject itself — exposes destroy(). Calling it on an object you looked up (for example gameObject.getObject(id).destroy()) removes that object, not the one the current script is attached to. From a scene script, scene.destroyObject(id) and scene.destroyObjectByRef(object) do the same thing by id or by reference.

All three remove any scene object, authored or runtime-created, together with every object parented under it. Removal is deferred until the current callback (update, onTriggerEnter, etc.) finishes, so it is safe to destroy an object from inside its own collision or trigger handler. Each returns true when the object was queued for removal and false when there was nothing to do — the id does not resolve, the object is already on its way out, or the simulation is not running.

Authored objects come back on stop

An authored object removed at runtime returns automatically when simulation stops, so the scene file itself is never modified. An object created with scene.createObject() is gone for good once destroyed.

Destroying objects in a multiplayer room

Only scripts that run on the room server — the scene script, characters' and vehicles' control scripts, and objects declared "authority" — may destroy an object, and players see it disappear only if it is shared with them: a slotted character or vehicle, a scripted object declared "authority", an unscripted object with Replicate turned on, or a copy made with multiplayer.spawn. To make breakable crates work in a room, turn on each one's Replicate setting, or give it a script and declare it "authority" (at most 32 such props per scene). See Build and test a multiplayer game.

Example: destroy nearby crates

This is the simplest pattern for a Ground Pound ability. Name the breakable rigid bodies with a shared prefix such as Ground Pound Crate, keep them dynamic, and call destroyNearbyCrates() once when the character lands. If the controller already defines init(), merge the crate lookup into that function instead of defining a second one.

local GROUND_POUND_RADIUS = 1.5
local CRATE_NAME_PREFIX = "Ground Pound Crate"

local crateIds = {}

function init()
    local crates = gameObject.getObjectsByNamePrefix(CRATE_NAME_PREFIX)
    for _, crate in ipairs(crates) do
        crateIds[crate.id] = true
    end
end

function destroyNearbyCrates()
    local nearbyIds = gameObject.sphereQuery(GROUND_POUND_RADIUS)

    for _, id in ipairs(nearbyIds) do
        if crateIds[id] then
            local crate = gameObject.getObject(id)
            if crate then
                crate.destroy()
                crateIds[id] = nil
            end
        end
    end
end

Clearing the id from crateIds after destroying it isn't strictly required — a destroyed object's id simply won't resolve again — but it keeps the table from growing stale across many stomps.

Alternative: knock crates away with an impulse

Use an impulse instead of destroy() when the crate should go flying rather than disappear — for example if it should still be collectible or collidable afterward.

function knockBackNearbyCrates()
    local playerPosition = gameObject.getPosition()
    local nearbyIds = gameObject.sphereQuery(GROUND_POUND_RADIUS)
    local hitIds = {}

    for _, id in ipairs(nearbyIds) do
        if crateIds[id] and not hitIds[id] then
            hitIds[id] = true

            local crate = gameObject.getObject(id)
            if crate then
                local cratePosition = crate.getPosition()
                local dx = cratePosition.x - playerPosition.x
                local dz = cratePosition.z - playerPosition.z
                local distance = math.sqrt(dx * dx + dz * dz)

                if distance < 0.001 then
                    dx, dz, distance = 1, 0, 1
                end

                crate.addImpulse(
                    dx / distance * 20,
                    8,
                    dz / distance * 20
                )
            end
        end
    end
end

The impulse values are a starting point for a mass-5 crate. Increase or decrease them to match the desired launch speed. addForce and addTorque are also available on the resolved object API.

The same three calls work if the thing you resolved is a soft body, but the numbers above will not visibly move one. A soft body's mass is its vertex count rather than its Mass field, so the default soft sphere weighs 162 kg against this crate's 5 — an impulse of 20 becomes about 0.12 m/s. Scale by the vertex count. See Physics, collisions, and triggers.

Share state between object scripts

A resolved object API exposes getVar(name) and setVar(name, value). Define the variable on the target object in the inspector first, then another script can update it:

local crate = gameObject.getObject(crateId)
if crate then
    crate.setVar("broken", true)
end

The crate's own script can read gameObject.getVar("broken") during update(deltaTime). Primitive values are numbers, strings, and booleans. An editor-configured object_ref variable is returned as another object API.

In a multiplayer room only scripts on the room server may call setVar, and object variables are not replicated: each browser keeps its own copy, so a value set on the server never reaches players. Use multiplayer state for anything players need to see; see Build and test a multiplayer game.

Trigger callback payloads

Call gameObject.registerForCollisions() in init(), then define onTriggerEnter(other) and onTriggerExit(other). Trigger callbacks can run on scripts attached to either side of the overlap. A script that defines no onTriggerEnter or onTriggerExit receives the overlap in onCollisionEnter or onCollisionExit instead, so an existing collision script keeps working when its object becomes a trigger.

function init()
    gameObject.registerForCollisions()
end

function onTriggerEnter(other)
    print("Entered trigger: " .. other.id)

    local object = gameObject.getObject(other.id)
    if object then
        object.setVar("insideHazard", true)
    end
end

function onTriggerExit(other)
    print("Left trigger: " .. other.id)
end

other is a callback snapshot, not an object API. On entry it contains id, position, rotation, normal, contactPoint, contactDepth, and isTrigger. On exit, rely on id, position, and rotation. Resolve other.id when you need current state or object methods.

Calling a method on the snapshot directly — other.setPosition(...), other.setVar(...) — raises, because those fields are nil on a plain table. The error stops the callback where it stands, so the effect you expected never happens and the overlap looks as though it was never detected. See Troubleshooting for how to confirm which of the two it is.

Combining a trigger volume with destroy() is the other common way to break a crate: give the crate an isSensor child (or make the crate itself a trigger the player's stomp overlaps), then call gameObject.destroy() from that object's own onTriggerEnter(other) when other.id is the player.