Builtin Library
README.md
physics
Convenience wrapper around the __physics FFI namespace. Covers
raycasting, force / impulse / torque application, velocity reads and
writes, body-property mutators (mass, damping, body type, locks),
collision groups, joints, constraints, colliders, wheel-collider
helpers, and the observation reads that report what the solver holds
for a body and why it is not moving it, in one consistent surface.
Exposed as the Physics global via --!global Physics.
Exports
Queries:
Physics.raycast(origin, direction, maxDistance?, exclude?) -> table?Physics.raycastAll(origin, direction, maxDistance?, maxHits?) -> tablePhysics.overlapSphere(center, radius) -> tablePhysics.sphereCast(origin, radius, direction, maxDistance?, exclude?) -> table?Physics.capsuleCast(origin, radius, halfHeight, direction, maxDistance?, exclude?) -> table?Physics.boxCast(origin, halfExtents, direction, maxDistance?, exclude?) -> table?Physics.getGravity() -> vec3/Physics.setGravity(g)Physics.getVelocity(entityId?) -> vec3?/Physics.getAngularVelocity(entityId?) -> vec3?
Forces / impulses (each accepts either (vec3) for the script-context entity or (entityId, vec3); a vec3 may be an {x, y, z} table or three loose numbers):
Physics.applyForce/Physics.applyForceAtPoint/Physics.applyTorque: from code that runs inside a step (Physics.onBeforeStep/onAfterStep, a contact listener,fixedUpdate) they act on the next step alone (call once per step); from anywhere else they act over the next frame's simulated time (call once per frame). A continuous force integrates to force times time at any frame rate and tick ratePhysics.applyImpulse— one-shot velocity changePhysics.setVelocity/Physics.addVelocity/Physics.setAngularVelocity
Step hooks (at the physics tick rate, however many steps a frame runs):
Physics.onBeforeStep(fn) -> off:fn(dt, step)at the start of every step, after its force reset; a force written there acts on that step, and a read sees the state it starts from (springs, fields, thrusters, controllers)Physics.onAfterStep(fn) -> off:fn(dt, step)once the step has run, after its contact events and beforefixedUpdate; a read sees what that step left- One engine call runs every hook of a side, and a hook ends with
off()or with the component that registered it
Body properties:
Physics.setGravityScale,Physics.setMass,Physics.setLinearDamping,Physics.setAngularDamping,Physics.setCcdEnabledPhysics.setBodyType(entityId, "dynamic" | "kinematic" | "static")Physics.setRotationLocks(entityId, x, y, z)/Physics.setTranslationLocks(entityId, x, y, z)
Surfaces (what colliders are made of):
Physics.defineSurface(name, { friction?, restitution?, rollingResistance?, spinningResistance?, tags? })/Physics.removeSurface(name)/Physics.getSurface(name) -> PhysicsSurface?/Physics.surfaces(): a named material a collider takes by naming it in itssurfacefield, and a heightfield per sample throughsurfaceLayers/surfaceMap(a terrain's painted layers each collide as their own surface)rollingResistancec: a body rolling on the surface slows at c times gravity on level ground, stopping after v^2 / (2 c g), whatever its mass and shape;spinningResistancedoes the same for a body spinning about the contact normal. Both act through contacts only, so a ball in flight keeps its spin- Per contact friction and restitution combine as they always have (the average), and the two sides' rolling and spinning resistances add, so a ball of none on a floor of c rolls against c
- Ray and shape cast hits carry
surface; contacts, contact points, contact events andPhysics.contactPeakcarrysurfaceandotherSurface;Physics.groundContact(entityId, maxSlope?) -> PhysicsGroundContact?names what a body stands on and the surface under it
Surfaces that act on what touches them (collider fields, read by the physics step itself, no script per contact):
oneWay = { x, y, z }: the direction, in the collider's own frame, bodies pass through it along; a body meeting it from that side stands on it (a jump-through platform is{ 0, 1, 0 }).Physics.passThrough(entityId, throughId?)drops a standing rigid body throughsurfaceVelocity = { x, y, z }: the velocity, m/s in the collider's own frame, its surface moves at along itself while the collider stays put; friction carries what rests on it (conveyor belts, treadmills, escalators). Write the field at runtime to change itcontactTag = "name"withPhysics.setContactFilter(tagA, tagB, "ignore" | "sensor" | "solid")/Physics.contactFilters(): a pair of tags passes through each other ("ignore"), reports contacts and pushes nothing ("sensor"), or collides ("solid", every pair the filter does not name)
Collision groups & filtering:
Physics.setCollisionGroups(entityId, membership, filter)Physics.ignoreCollision(entityIdA, entityIdB, ignore?)
Joints / constraints / colliders:
Physics.addJoint(a, b, opts?)/Physics.removeJoint(id)/Physics.setJointMotor(id, vel, maxForce): joints with limits (limitMin/limitMax,swingLimit/twistLimit,axisLimits), motors (motorMode"velocity" / "position", capped bymotorMaxForce), joint frames (axis,remoteAxis),collideConnected, and a 6-DOFkind = "generic"withlockedAxesPhysics.jointState(id): a joint's angle / travel / swing and twist, velocity, whether it is at a limit, and the force its limit and motor appliedPhysics.jointReaction(id): the load (the stress) a joint carries as signed vectors, read along the member it holds (axialtension positive, compression negative,shear,bending,torsion, a rope'stension) and asload, the share of the break thresholds it is measured againstPhysics.jointReactions(targets?, opts?)/Physics.listenJointLoads(targets, listen?): many joints' loads in one call as arrays, and the peak each carried between two readsPhysics.addConstraint(id, opts?)/Physics.removeConstraint(id, index?)Physics.addCollider(id, config)/Physics.removeCollider(id)Physics.addWheelCollider(id, config?)/Physics.removeWheelCollider(id)/Physics.getWheelState(id)/Physics.vehicleState(chassisId): the wheels under one rigid body make up that body's ray-cast vehicle; theWheelCollidercomponent README covers the fieldsPhysics.detachWheel(id)/Physics.attachWheel(id): a wheel comes off its vehicle as a dynamic body of its own (rolling and bouncing with its spin, the vehicle driving on the wheels it has left) and goes back on its mount; abreakLoadtakes it off by itself, asolidwheel is a volume other bodies strike, andvehicleStatelists the wheels that came off underdetachedWheels
Observation (read off the solver, not off the Physics component):
Physics.observe(entityId?, opts?) -> PhysicsObservation?— one read of the world, or of one body;bodiesis an array whose entries each name their own entityPhysics.worldState() -> PhysicsWorldState— bodies by type, awake / asleep, colliders, soft bodies, joints, contact pairs and points, gravity, timestep, and the last step's costPhysics.softBodyState(entityId, opts?) -> PhysicsSoftBodyState?— a soft body's particle and cell counts, volume against rest, frame, bounds, sleep state and contacts, the solver simulating it (solver:"constraints"or"fem"), its particles' positions with{ positions = true }, the cage ashape = "mesh"body was filled with (cage: cell size, whether it came from the cache, build time, parts filled and left out), and where the body stands:statusis"built","pending"(the component stands and the body is built at the start of the next frame, so a read in the call that added it answers this;waitingFornames what the build waits on),"refused"(refusalsays why the component was not built) or"none"Physics.debugLines(opts?) -> PhysicsDebugLines?: what the physics world simulates beyond collider surfaces, as world-space lines with the data behind them: a soft body's elements coloured by load (cage), its free, pinned and attached particles (particles), soft-soft contacts (contacts), and eachWheelColliderwheel's suspension ray, travel, tyre and ground contact (wheels).capture { pass = "physics" }and the edit-mode collider overlay draw these lines;Physics.softBodyState(id).diagnosticsreads a soft body's inverted cells, self-crossings, element loads (maxLoad,loadedElements), pinned and attached particles as numbersPhysics.drawnPose(entityId) -> PhysicsDrawnPose?: where an entity is drawn this frame (position,rotation) beside the pose its transform holds (simulatedPosition,simulatedRotation, whatentity.positionreports). A dynamic body withPhysics.interpolationon is drawn blended between its last two physics ticks, and so is everything parented under it:interpolated,interpolatedBy(the body whose blend places it) andalpha(how far into the current tick the frame sits) say so. A camera or anything else that has to stay with a moving body on screen reads thisPhysics.bodyState(entityId) -> PhysicsBodyState?— the mass, inertia, gravity scale, damping, locks, CCD, collision groups, sleep timers, velocities, queued force and torque, colliders, contacts, joints and transform constraints the solver holdsPhysics.whyStill(entityId) -> (string?, string?)— the one reason the solver is not advancing a body, and the detail behind itPhysics.stillnessReasons() -> {string}— every reasonwhyStillcan answer withPhysics.contacts(entityId) -> {PhysicsContact}/Physics.touching(a, b) -> (boolean, number, {PhysicsContactPoint})Physics.catchUp() -> PhysicsCatchUp: how the frame clock kept up with real time: the frames it ran, those that dropped simulated time, owing more than a frame may catch up (boundSeconds, frommaxCatchUpSeconds) or with steps pastmaxSubstepsthat fell behind the time they cover (framesBehind), and the simulated seconds they dropped, counted since the engine startedPhysics.stepCost() -> PhysicsStepCost?: the last step's per-stage cost, with the share attributed to soft bodies (softBodyMs) and each soft body's part of it (softBodies: entity, ms, particles, cells, elements, contacts, substeps, costliest first); a soft body's own read carries its part aslastStepMsPhysics.softBodySettings(changes?) -> PhysicsSoftBodySettings: the settings every soft body runs under, each trading quality for cost (maxExtraSubstepRate,contactStiffening,resweepStrain,tangleDetection,overlapConstraints,recoveryPace, and the FEM solver'sfemTolerance,femMaxIterations,femMaxDenseDofs, read by the bodies whoseSoftBody.solveris"fem"); with a table, changes the fields it names, saved asphysics.soft_body_*world settings and applied from the next tick, and refuses an unknown field or a value out of range by namePhysics.solverSettings(changes?) -> PhysicsSolverSettings: the settings the constraint solver runs every step under (tickRate,maxCatchUpSeconds,maxSubsteps,solverIterations,pgsIterations,stabilizationIterations,warmStartCoefficient,warmStartJoints,lengthUnit,contactFrequency,contactDampingRatio,staticContactFrequency,staticContactDampingRatio,allowedLinearError,maxCorrectiveVelocity,predictionDistance,ccdSubsteps,frictionModel,frictionInBiasPass); with a table, changes the fields it names, saved asphysics.*world settings and run by the solver from the next step, and refuses an unknown field or a value out of range by name
High-level helpers:
Physics.raycastBetween(fromId, toId, maxDistance?) -> table?(ids or proxies)Physics.hasLineOfSight(fromId, toId) -> boolean(ids or proxies)
Usage
-- `Physics` is auto-injected by the prelude — no require in user code.
local hit = Physics.raycast({x=0,y=2,z=0}, {x=0,y=-1,z=0})
Physics.applyImpulse(entityId, {x=0, y=5, z=0})
Physics.addJoint(a, b, { kind = "fixed" })
Notes
- Most apply/set helpers are polymorphic on the first argument: they accept either an explicit
entityId(with the value as a second arg) or a value directly (using the script-context entity). addJointaccepts both vec3-style anchor tables (localAnchor = {x,y,z}) and pre-split scalar keys (localAnchorX/Y/Z); both forms forward as split keys to the Joint component.setCollisionGroupsadds theCollisionGroupcomponent if missing.getWheelStatereturnsnilwhen the entity has noWheelCollidercomponent, andvehicleStatereturnsnilwhen noWheelCollidersits under the chassis.- The sweeps (
sphereCast,capsuleCast,boxCast) reportdistanceas how far the shape's centre travels before its surface meets the geometry, andnormalas the outward normal of the surface it met — the same normal a ray hit carries. - A ray's or a sweep's
directionmay have any length: the engine normalises it, somaxDistanceand the reporteddistanceare world units whatever the direction's length, and a direction of zero length hits nothing. excludeis applied while searching: the first hitraycastreports, and every hitraycastAllcounts towardmaxHits, is on an entity outside the list, however many excluded colliders lie in front of it.- Every hit table carries
startedInside, and it says which surfacenormaldescribes.false— the query travelled to the collider and crossed its surface, sodistanceis how far it went,pointis where it met the surface, andnormalis that surface's outward normal.true— the query's own start already lay inside that collider, sodistanceis 0,pointis the start itself, andnormalis the collider's outward surface normal at the surface point nearest the start: the shortest way out of it.normalis a unit vector either way, and every collider shape answers an inside start the same way, so a slope read likemath.acos(hit.normal.y)is meaningful without knowing which shape was hit. raycastBetweenandhasLineOfSightcast from the source's world position toward the target's world position, wherever each sits in the hierarchy. Each end takes either an entity id or a proxy, and an end naming no entity raises an error naming the argument (fromIdortoId) and the id.- Each end's own colliders hold nothing up: those of the body it rides (its own
PhysicsorSoftBody, else the body of its nearest ancestor carrying one) and of every entity riding that body, a child collider composed onto it included.raycastBetweenpasses over the source's; with nothing between the pair, its first hit is the target's collider. raycastBetweenreturnsnilif the two entities are coincident (< 0.001 units apart).hasLineOfSightreturnstrueeven when nothing is hit: line-of-sight only fails if a collider belonging to neither end sits between the pair.- The observation reads answer in edit mode as well as play mode.
bodyStatereportsexists = falsewithstillness = "noBody"for an entity that carries no rigid body, andnilonly when nothing in the scene answers to that id. observe(nil, { bodies = false })builds the world accounting alone;{ contactPoints = false }keeps each contact pair's normal, depth, impulse and point count while leaving out the individual points.stepCostfigures cover the one step that ran, so they are already per-step costs; it isnilon a frame where the pipeline did not step.