Dicey Table

Welding

Weld is a second mode for a parented assembly, alongside the default attached mode covered on Parenting. Where an attached assembly is a group of kinematic children following one parent, a welded assembly is genuinely one physics body.

<!-- screenshot: editor/working/welding.png -->

Physics caveat. Weld changes what is actually simulated, not just how the Inspector displays the assembly. Read the mechanics below before enabling it on anything you plan to throw dice at.

Turning it on#

Weld children is a checkbox in the parent's RIGIDBODY section, and it is offered only when the entity actually has children (TableEditModeShell.tsx, the Weld children toggle inside the Inspector's RIGIDBODY collapsible) — there is nothing to weld otherwise. It writes physics.weldChildren on the parent (packages/shared/src/objectComponents.ts schema; the flag itself lives on ObjectPhysics).

What weld changes, mechanically#

Enabling weld (TabletopRuntime.applyWeldState, TabletopRuntime.ts):

  • The parent's collision component becomes compound.
  • Every descendant keeps its collision component but loses its rigidbody — a descendant with its own body would be simulated separately and contribute to the compound, which has no defined meaning, so the body is removed.
  • Ammo then treats every descendant's collision shape as a child shape of the parent's single compound shape: one body, with combined mass and inertia.

The practical difference from attached mode: an attached assembly is kinematic, so it shoves loose pieces around but is never pushed back by them (see Parenting). A welded assembly is one dynamic body — a die thrown at it bounces off and nudges it, the same as it would nudge any other loose piece.

Weld and authored trigger volumes cannot coexist, and that is an engine constraint rather than a policy. Building a compound adopts every rigidbody-less descendant collision under the parent — which is exactly what a trigger volume is, so a volume inside a welded assembly would silently become solid and start blocking pieces. Volumes are therefore torn down before a weld and rebuilt on unweld, with a warning logged once. A model whose trigger volumes carry game rules should not be welded into an assembly.

The compound shape rebuilds, debounced to once per frame#

The compound shape has to be rebuilt whenever a child is added, removed, moved or rescaled. That rebuild is queued and debounced to once per framequeueWeldRebuild / flushWeldRebuilds (TabletopRuntime.ts) — and explicitly never per drag frame: the queue collects a parentId and the actual applyWeldState call happens once from update(), however many times the parent's edge changed within that frame. Forcing a rebuild is also necessary on every weld/unweld transition even when the collision type stays compound, because PlayCanvas's own collision.type setter is a no-op when the assigned value is unchanged (recreateCollisionShape, TabletopRuntime.ts, calls the engine's recreatePhysicalShapes directly to force it).

Do not read a performance claim into "debounced." The debounce bounds how often a rebuild happens, not how expensive one is. The actual compound-rebuild cost in this Ammo build is unmeasured — this page states the mechanism, not a performance verdict. Do not extrapolate one from the other.

Unweld restores each child's own body — and its authored physics#

Turning weld off is not just "give the rigidbody back." applyWeldState's unweld branch (TabletopRuntime.ts) does three things per child, in order:

  1. Clears the engine's private _compoundParent reference before the parent's shape stops being a compound — leaving it set would make the next shape rebuild on the child throw inside the engine (there is no public "leave this compound" API, so this is the documented-by-source workaround).
  2. Re-adds a rigidbody component if the child does not have one.
  3. Re-applies the child's authored physics (applyPhysicsOverrideToObject) — mass, friction, restitution, damping, body type, collision shape. Skipping this step would leave every unwelded child with only createRigidbodyConfig's generic per-kind defaults until the scene reloaded, silently discarding whatever you had tuned before welding.

Nested weld is rejected, with a reason#

Welding a member of an already-welded assembly is refused outright rather than half-supported (TabletopRuntime.ts):

"Cannot weld <label>: it is already part of a welded assembly."

Why: PlayCanvas reassigns a child's _compoundParent to the nearest compound ancestor when shapes are rebuilt, which would silently pull the inner sub-assembly out of the outer one while grab escalation and isWeldedMember still reported the outer parent — physics and the UI would disagree about which assembly a piece belongs to. Rather than ship that, the runtime deletes the just-set weldChildren flag and refuses.

Where that reason goes today. The rejection is written through the same log path every system message uses — this.log("System", …), which appends a TableEvent and calls callbacks.onEvent. On the live table that reaches the activity log. In Edit Mode it currently does not reach anywhere: TableEditModeShell.tsx wires the canvas's onEvent to a no-op (onEvent={() => undefined}), so the reason string above is generated but discarded while authoring. See Escalations below — this is a real gap between what the feature intends ("surface the reason") and what Edit Mode currently does with it, not a documentation choice.

One consequence worth knowing while that gap stands: because the Inspector's Weld children checkbox reads from the editor's own scene draft (object.physics?.weldChildren), not from the runtime's post-rejection state, toggling weld on a nested assembly can leave the checkbox showing on in the Inspector even though the runtime rejected it and the assembly is not actually welded. Toggling it off and back on, or reselecting the entity, will not reconcile this on its own — reload the scene if you suspect this has happened. Verify a weld took effect by checking ENTITY (ADVANCED) — a genuinely welded child's Live Entity carries no rigidbody component in its chip list.

The one-line decision rule#

Parenting gives a child a parent transform (and makes it kinematic). Weld merges children into the parent's single compound rigidbody. Joints connect two independently-dynamic bodies with an Ammo constraint.

If you are choosing between these three for an assembly you're building: reach for parenting first (it's the default and the cheapest), reach for weld when you need the assembly to actually receive physical impacts as one body, and reach for a Fixed Joint when the pieces must each keep being their own independently-simulated dynamic body.

Unvalidated behavior#

Unvalidated. The physics-jitter pass on a parented/welded assembly and any multi-peer weld/unweld round trip (does a peer that joins mid-weld see the same compound shape as the host?) have not been walked in a browser. Everything above is derived from reading TabletopRuntime.ts directly. Treat behavior under real network conditions or a live physics stress test as code-derived, not human-confirmed.

See also#