Builtin Library
README.md
Physics
Adds rigid body dynamics to an entity. Body kind is "dynamic" (affected by forces), "static" (immovable), "kinematic" (placed by script at the pose its transform holds) or "kinematicVelocity" (carried by the solver at the velocity script gives it with Physics.setVelocity). Both kinematic kinds push dynamic bodies and are not pushed back.
Public fields: kind, gravityScale, mass, linearDamping, angularDamping, ccd, interpolation, lockRotationX/Y/Z, lockTranslationX/Y/Z, dominance, canSleep, sleepLinearThreshold, sleepAngularThreshold, centerOfMass, inertia, softCcd, extraSolverIterations, extraPgsIterations. Every field reaches the solver on the next step when written, and Physics.bodyState(id) reads each back as the solver holds it. A value outside what a field takes is refused with an error naming the field.
dominance (-127 to 127) decides who pushes whom between dynamic bodies: a body in a higher group pushes one in a lower group and is not pushed back, the way a player character shoves crates without slowing. Bodies in one group push each other as usual.
canSleep = false keeps the body simulated every step however slowly it moves (a probe coasting in zero gravity, a vehicle idling at rest); sleepLinearThreshold (m/s at the body's farthest point, default 0.05) and sleepAngularThreshold (rad/s, default 0.5, for a body with no colliders) set how still it must stay for half a second before it sleeps. Physics.sleep(id) puts a body to sleep at once.
centerOfMass (local, metres) and inertia (principal moments, kg·m²) replace the values the colliders give the body; leave them unset to derive them. A car body with centerOfMass = { 0, -0.5, 0 } resists the roll that tips it with the derived centre.
softCcd (metres) predicts contacts that far ahead of the body each step, which catches a fast body before it passes through a thin wall at a fraction of the cost of ccd. Set it to about the distance the body travels in one step (speed / tick rate).
extraSolverIterations and extraPgsIterations (0 to 16 each) stiffen one structure without raising the solver world-wide: the island the body is part of (everything jointed to it or touching it) runs the world's solverIterations plus the largest extra substeps any of its bodies asks for, and the world's pgsIterations plus the largest extra passes. Put the same values on every member of a bridge, a chain or a ragdoll; the island's step costs about that many more substeps, and the rest of the world costs what it did.
ccd (continuous collision detection) sweeps the body along its motion so it cannot pass through thin geometry between two steps. It is off by default: the sweep runs on every body that travels further than its own thickness in one step — most of a collapsing structure or a debris burst — and it dominates the physics budget when a scene has many such bodies. Turn it on for the individual bodies that would visibly tunnel: bullets, thrown props, a wrecking ball.
entity(id).component.add("Physics", { kind = "dynamic", ccd = true })
A body on an entity under a parent lives in world space. It starts where its entity stands in the world, with the parent's position, rotation and scale applied, and a write to the entity's position, localPosition, rotation or localRotation places the body where that write puts the entity, in the same call: a ray cast right after the write meets it there. The one exception is a kinematic body while the clock runs: the write aims it, and the next step sweeps it there, carrying the dynamic bodies in its path. While the clock runs, the solver moves a dynamic or kinematicVelocity body and its parent does not carry it: moving or turning the parent leaves the body where the solver has it, and each step writes the entity's local transform from the body's pose, so the entity stays on the body. A static or kinematic body goes where its entity goes, its parent's moves included. With the clock held (edit mode, a paused session), every body goes where its entity goes. A change of parent that keeps the entity's local pose (setParent or unparent with keepWorldTransform = false) is a write of that kind: the entity's local pose read under its new parent (or as world, for unparent) places its body, every kind of body, whether the clock runs or is held, as a write to its localPosition does. The placement stops a dynamic body and leaves a kinematicVelocity body the velocity it was given; a kinematic body while the clock runs is aimed there, as for any write. keepWorldTransform = true leaves the entity where it stands in the world, and its body where it is, at the velocity it has. Colliders on the entity's children go with its body.
mass is the body's total mass in kilograms. At its default of 0 the mass comes from the space the body's solid colliders fill, at 1 kg per cubic metre, so a bigger collider makes a heavier body; a value greater than zero sets the total mass exactly. Either way the mass is spread evenly over that space: where two colliders overlap, the space they share is counted once, so a ball inside a box weighs what the box weighs, and a trigger (isTrigger = true) fills no space and adds no mass, inertia or shift of the centre of mass.
A body's colliders are the ones on its entity and on every entity beneath it that carries no Physics of its own, whatever order the components were added in: a collider added to a child before its parent got Physics, or a child moved under the body later, joins the body, and a child moved out from under it, or whose body's Physics is removed, stands alone as static geometry. The body's mass, centre of mass and inertia come from the shapes, poses and sizes of those colliders (a mass above zero is spread over them), so a car built from a chassis and child parts rolls and pitches the way its parts are laid out. Physics.bodyState(id).colliders lists them.
A body whose entity holds no collider is given a stand-in box: one fitted to the entity's model, or a 1 m box (half extents 0.5) when it renders none. It collides with everything and Physics.bodyState(id).colliders lists it. A collider component added to the entity replaces it, and the first solid collider (one that is no trigger) on an entity beneath the body takes its place as it joins the body, in the same frame when both are added together. A body whose colliders beneath it are still waiting for their mesh, or sit on an inactive entity, rests on the stand-in until one of them is built. The stand-in is given as the body is built: a built body whose last collider is removed or deactivated, or moves out from under it, holds no collider until another one joins it.
Methods: :applyForce(x, y, z), :applyImpulse(x, y, z), :applyTorque(x, y, z), :setVelocity(x, y, z), :addVelocity(dx, dy, dz).
entity(id).component.add("Physics", { kind = "dynamic" })
entity(id).component.get("Physics"):applyImpulse(5, 0, 0)