Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

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?) -> table
  • Physics.overlapSphere(center, radius) -> table
  • Physics.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 rate
  • Physics.applyImpulse — one-shot velocity change
  • Physics.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 before fixedUpdate; 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.setCcdEnabled
  • Physics.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 its surface field, and a heightfield per sample through surfaceLayers / surfaceMap (a terrain's painted layers each collide as their own surface)
  • rollingResistance c: 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; spinningResistance does 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 and Physics.contactPeak carry surface and otherSurface; 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 through
  • surfaceVelocity = { 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 it
  • contactTag = "name" with Physics.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 by motorMaxForce), joint frames (axis, remoteAxis), collideConnected, and a 6-DOF kind = "generic" with lockedAxes
  • Physics.jointState(id): a joint's angle / travel / swing and twist, velocity, whether it is at a limit, and the force its limit and motor applied
  • Physics.jointReaction(id): the load (the stress) a joint carries as signed vectors, read along the member it holds (axial tension positive, compression negative, shear, bending, torsion, a rope's tension) and as load, the share of the break thresholds it is measured against
  • Physics.jointReactions(targets?, opts?) / Physics.listenJointLoads(targets, listen?): many joints' loads in one call as arrays, and the peak each carried between two reads
  • Physics.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; the WheelCollider component README covers the fields
  • Physics.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; a breakLoad takes it off by itself, a solid wheel is a volume other bodies strike, and vehicleState lists the wheels that came off under detachedWheels

Observation (read off the solver, not off the Physics component):

  • Physics.observe(entityId?, opts?) -> PhysicsObservation? — one read of the world, or of one body; bodies is an array whose entries each name their own entity
  • Physics.worldState() -> PhysicsWorldState — bodies by type, awake / asleep, colliders, soft bodies, joints, contact pairs and points, gravity, timestep, and the last step's cost
  • Physics.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 a shape = "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: status is "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; waitingFor names what the build waits on), "refused" (refusal says 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 each WheelCollider wheel's suspension ray, travel, tyre and ground contact (wheels). capture { pass = "physics" } and the edit-mode collider overlay draw these lines; Physics.softBodyState(id).diagnostics reads a soft body's inverted cells, self-crossings, element loads (maxLoad, loadedElements), pinned and attached particles as numbers
  • Physics.drawnPose(entityId) -> PhysicsDrawnPose?: where an entity is drawn this frame (position, rotation) beside the pose its transform holds (simulatedPosition, simulatedRotation, what entity.position reports). A dynamic body with Physics.interpolation on is drawn blended between its last two physics ticks, and so is everything parented under it: interpolated, interpolatedBy (the body whose blend places it) and alpha (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 this
  • Physics.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 holds
  • Physics.whyStill(entityId) -> (string?, string?) — the one reason the solver is not advancing a body, and the detail behind it
  • Physics.stillnessReasons() -> {string} — every reason whyStill can answer with
  • Physics.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, from maxCatchUpSeconds) or with steps past maxSubsteps that fell behind the time they cover (framesBehind), and the simulated seconds they dropped, counted since the engine started
  • Physics.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 as lastStepMs
  • Physics.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's femTolerance, femMaxIterations, femMaxDenseDofs, read by the bodies whose SoftBody.solver is "fem"); with a table, changes the fields it names, saved as physics.soft_body_* world settings and applied from the next tick, and refuses an unknown field or a value out of range by name
  • Physics.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 as physics.* 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).
  • addJoint accepts 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.
  • setCollisionGroups adds the CollisionGroup component if missing.
  • getWheelState returns nil when the entity has no WheelCollider component, and vehicleState returns nil when no WheelCollider sits under the chassis.
  • The sweeps (sphereCast, capsuleCast, boxCast) report distance as how far the shape's centre travels before its surface meets the geometry, and normal as the outward normal of the surface it met — the same normal a ray hit carries.
  • A ray's or a sweep's direction may have any length: the engine normalises it, so maxDistance and the reported distance are world units whatever the direction's length, and a direction of zero length hits nothing.
  • exclude is applied while searching: the first hit raycast reports, and every hit raycastAll counts toward maxHits, 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 surface normal describes. false — the query travelled to the collider and crossed its surface, so distance is how far it went, point is where it met the surface, and normal is that surface's outward normal. true — the query's own start already lay inside that collider, so distance is 0, point is the start itself, and normal is the collider's outward surface normal at the surface point nearest the start: the shortest way out of it. normal is a unit vector either way, and every collider shape answers an inside start the same way, so a slope read like math.acos(hit.normal.y) is meaningful without knowing which shape was hit.
  • raycastBetween and hasLineOfSight cast 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 (fromId or toId) and the id.
  • Each end's own colliders hold nothing up: those of the body it rides (its own Physics or SoftBody, else the body of its nearest ancestor carrying one) and of every entity riding that body, a child collider composed onto it included. raycastBetween passes over the source's; with nothing between the pair, its first hit is the target's collider.
  • raycastBetween returns nil if the two entities are coincident (< 0.001 units apart).
  • hasLineOfSight returns true even 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. bodyState reports exists = false with stillness = "noBody" for an entity that carries no rigid body, and nil only 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.
  • stepCost figures cover the one step that ran, so they are already per-step costs; it is nil on a frame where the pipeline did not step.