Assemblies: slots, parts and swapping them at runtime
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 thekindof part it takes (wheel,engine), the assettagsa part must carry, adefaultpart, and optionally ajointthat binds the part's body to the body the slot is on.Part: on the root of a part's bundle,fitslists 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. Itsconfigsays 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,emptyandconfigureis 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.