Log inGet started

Assemblies: slots, parts and swapping them at runtime

Updated October 26, 2026
  • Slot: a named place a part fills. It goes on an (usually empty) child entity standing where the part attaches: the part is built as a child of that entity, at its transform. It states the kind of part it takes (wheel, engine), the asset tags a part must carry, a default part, and optionally a joint that binds the part's body to the body the slot is on.
  • Part: on the root of a part's bundle, fits lists the slot kinds the part fits. A part from another world's package carries this with it.
  • Assembly: on the root the slots belong to. Its config says what fills each slot, by the slot's path, and its methods fill, empty, swap, resolve and explain.

Humanoid role sockets (righthand, head, ...) are slots of the same family: an Assembly on a humanoid lists them, and filling one seats the part the way Humanoid:attach does.

Build an assembly

-- Parts: one bundle each, with a Part saying which slots it fits
local function makePart(name, kind, meshEntity)
    meshEntity.component.add("Part", { fits = { kind } })
    return asset.create("bundle", name, { entity = meshEntity })
end

-- The body: an Assembly on the root, a Slot on each mount point
local body = entity.spawn("Buggy")
body.component.add("Assembly")
for _, m in ipairs({
    { "wheel_fl", "wheel", { x = -0.8, y = 0.3, z =  1.2 } },
    { "wheel_fr", "wheel", { x =  0.8, y = 0.3, z =  1.2 } },
    { "engine",   "engine", { x = 0, y = 0.5, z = 1 } },
    { "seat",     "seat",   { x = 0, y = 0.6, z = 0 } },
}) do
    local mount = entity.spawn(m[1], { parent = body.id, position = m[3] })
    mount.component.add("Slot", { kind = m[2] })
end

-- Save the body as a bundle; every placed instance carries the slots
local buggy = asset.create("bundle", "Buggy", { entity = body })

Place it the usual way (entity.spawn("car").component.add("Asset", { source = buggy })): the Assembly and the slots arrive with it.

Fill, swap and read it

local asm = car.component.get("Assembly")

asm:configure({
    wheel_fl = "parts.SportWheel", wheel_fr = "parts.SportWheel",
    engine = { part = "parts.V8", turbo = "parts.Turbo" },   -- a part's own slots nest
    seat = "parts.BucketSeat",
})

asm:fill("wheel_fl", "parts.OffroadWheel")   -- rebuilds that wheel alone
asm:empty("seat")                            -- no seat, default included
asm:fill("seat", nil)                        -- back to the slot's default

for _, s in ipairs(asm:resolve()) do         -- what fills what, as a tree
    print(s.path, s.part, s.source, s.fill)
end

A slot's path is its name under the path of the slot whose part it stands in: the V8 bundle carries a turbo slot, so in the engine slot that slot is engine/turbo. The configuration holds every path, including slots of a part swapped away, so putting the V8 back brings its turbo back.

Swapping one part rebuilds that slot's part (and the parts in its slots) and nothing else: every other part keeps its entity and whatever state was written on it.

What fits, and why not

asm:fits("wheel_fl", "parts.V8")
-- false, "slot 'wheel_fl' takes parts of kind 'wheel'; 'parts.V8' fits { engine }"
asm:candidates("engine")       -- every bundle this world holds that fits
wheelSlot.tags = { "offroad" } -- the slot now takes only parts tagged offroad

fill raises with the same sentence for a part that does not fit, and names the slots there are for a path no slot answers to. configure checks every part first and changes nothing when one does not fit.

Undo, multiplayer and saving

config is a synced, saved field, so:

  • each fill, empty and configure is one undoable step;
  • the entity's owner changes it and every peer builds the same parts from it (another peer's call is refused, naming the owner);
  • it is saved with the scene, and the parts are rebuilt from it on load. The part entities are runtime children and are never written into the scene.

Joints

A slot whose joint names Joint fields ({ kind = "hinge", axis = { 1, 0, 0 } }) adds a Joint to the part's root, connected to the slot, so a part with its own body (a trailer, a door, a swinging arm) is held to the body the slot stands on by that joint. Without one, the part rides the slot as a child.

Where to read more

types/component covers components in general; the Slot, Part and Assembly component READMEs list every field and method, and the bundle type README covers saving an assembly or a part as a bundle.

  • documentation
  • guide