# `tecs.physics` Rigid-body simulation as part of the world. Install physics, spawn an entity with a [`Transform2D`](tecs.ecs.Transform2D), and attach a body: ```nupp tecs.physics.install(world, {gravity = {0.0, 980.0}}) local ground = world:spawn(tecs.ecs.Transform2D(320, 460)) tecs.physics.attach(world, ground, {kind = "static", halfWidth = 320, halfHeight = 20}) local crate = world:spawn(tecs.ecs.Transform2D(320, 100)) tecs.physics.attach(world, crate, {halfWidth = 16, halfHeight = 16, friction = 0.4}) tecs.physics.applyImpulse(world, crate, 400.0, 0.0) ``` The solver writes each body's `Transform2D` back, so nothing else has to move the entity. `detach` takes a body out of the solve without despawning its entity. On successful body creation, physics seeds [`PreviousTransform2D`](tecs.ecs.PreviousTransform2D) from the creation pose before publishing the solver's first result. The builtin snapshot system refreshes that history before subsequent fixed steps. Rendering interpolates position and the shortest rotation arc without changing simulation state. Public positions, extents, linear velocities, impulses, forces and gravity use pixels. Angles and angular velocities use radians. The solver works in meters through the `pixelsPerMeter` conversion. `Body` and `Collider` declare what the game requested. `Motion` preserves simulation state across pauses. Engine-owned `RigidBody` keeps the attachment visible to tools, and ordinary game code should ignore it. A secondary collider lives on its own entity and relates back through `ColliderOf`; a joint lives on its own entity and relates to its two bodies through `JointBodyA` and `JointBodyB`. The fixed phases own the simulation, and one fixed step is one crossing into the solver. Every mutation a game requests is queued and applied at the start of the step that follows it, and every read reports the state after the last completed step. That is what keeps the crossing batched: a world of ten thousand bodies costs one call per step rather than one call per body. A body declared during a frame becomes live during that frame's next fixed step, and its `RigidBody` row appears at the end of that step's `FixedUpdate`, which is where the solver hands back the slot it assigned. ## Submodules | Module | Description | | --- | --- | | `tecs.physics.contract` | The batched crossing between Tecs entities and a native rigid-body solver. | | `tecs.physics.rapier` | The Rapier implementation of the batched physics crossing. | ## Types ### `Body` _type_ ```nupp type Body = BodyValue ``` The value a `Body` column holds. ### `BodyOptions` _type_ ```nupp type BodyOptions = { kind: string?, halfWidth: number?, halfHeight: number?, radius: number?, capsuleLength: number?, segment: {number}?, offsetX: number?, offsetY: number?, density: number?, friction: number?, restitution: number?, categoryBits: integer?, maskBits: integer?, isSensor: boolean?, fixedRotation: boolean?, isBullet: boolean?, sleepEnabled: boolean?, gravityScale: number?, linearDamping: number?, angularDamping: number? } ``` The initial body and collider settings `attach` reads. `segment = {x1, y1, x2, y2}` selects a line collider in local pixels, useful for static map boundaries. It takes precedence over box or radius settings. Segment coordinates may be negative; endpoints must differ. ### `Collider` _type_ ```nupp type Collider = ColliderValue ``` The value a `Collider` column holds. ### `ContactBegin` _record_ ```nupp record ContactBegin entityA: integer entityB: integer end ``` `@derive(events.Event)` `@event(name="ContactBegin")` #### Fields ##### `entityA` ```nupp entityA: integer ``` Read-only. Identifies one body of the pair. ##### `entityB` ```nupp entityB: integer ``` Read-only. Identifies the other body of the pair. ### `ContactEnd` _record_ ```nupp record ContactEnd entityA: integer entityB: integer end ``` `@derive(events.Event)` `@event(name="ContactEnd")` #### Fields ##### `entityA` ```nupp entityA: integer ``` Read-only. Identifies one body of the pair. ##### `entityB` ```nupp entityB: integer ``` Read-only. Identifies the other body of the pair. ### `Joint` _type_ ```nupp type Joint = JointValue ``` The value a `Joint` column holds. ### `JointOptions` _type_ ```nupp type JointOptions = { kind: string?, anchorAX: number?, anchorAY: number?, anchorBX: number?, anchorBY: number?, axisX: number?, axisY: number?, limitMin: number?, limitMax: number?, motorTargetVelocity: number?, motorMaxForce: number?, contactsEnabled: boolean? } ``` The joint settings `attachJoint` reads. ### `Motion` _type_ ```nupp type Motion = MotionValue ``` The value a `Motion` column holds. ### `Options` _type_ ```nupp type Options = { gravity: {number}?, subStepCount: integer?, workerCount: integer?, simulation: contract.Simulation? } ``` The settings `install` reads before it creates a simulation. ### `QueryOptions` _type_ ```nupp type QueryOptions = { categoryBits: integer?, maskBits: integer? } ``` The collision filters a raycast reads. ### `RaycastHit` _record_ ```nupp record RaycastHit entity: integer x: number y: number normalX: number normalY: number fraction: number end ``` Reports where a ray first met a collider. #### Fields ##### `entity` ```nupp entity: integer ``` Read-only. Names the shape owner. A secondary collider reports its own entity rather than its body's. ##### `x` ```nupp x: number ``` Read-only. Holds the contact point along x, in world pixels. ##### `y` ```nupp y: number ``` Read-only. Holds the contact point along y, in world pixels, positive downward. ##### `normalX` ```nupp normalX: number ``` Read-only. Holds the outward unit normal along x, unscaled by pixels. ##### `normalY` ```nupp normalY: number ``` Read-only. Holds the outward unit normal along y. ##### `fraction` ```nupp fraction: number ``` Read-only. Holds the position along the segment, zero at its start and one at its end. ### `SensorBegin` _record_ ```nupp record SensorBegin sensor: integer visitor: integer end ``` `@derive(events.Event)` `@event(name="SensorBegin")` #### Fields ##### `sensor` ```nupp sensor: integer ``` Read-only. Identifies the sensor. ##### `visitor` ```nupp visitor: integer ``` Read-only. Identifies the body that entered it. ### `SensorEnd` _record_ ```nupp record SensorEnd sensor: integer visitor: integer end ``` `@derive(events.Event)` `@event(name="SensorEnd")` #### Fields ##### `sensor` ```nupp sensor: integer ``` Read-only. Identifies the sensor. ##### `visitor` ```nupp visitor: integer ``` Read-only. Identifies the body that left it. ### `SolverHandle` _struct_ ```nupp struct SolverHandle index1: integer world0: integer generation: uint32 end ``` `@derive(nupp.derive.Debug, nupp.derive.Serde)` Identifies one live slot in a simulation. Engine-owned and transient. `RigidBody`, `ColliderShape` and `JointLink` all use this shape so debug tools can read an attachment without knowing which arena it names. Ordinary game code should use `Body`, `Motion` and the functions on this module. #### Fields ##### `index1` ```nupp index1: integer ``` Engine-owned. Identifies the slot, one-based, and zero when the entity has no live counterpart. Ordinary game code should ignore it. ##### `world0` ```nupp world0: integer ``` Engine-owned. Identifies the simulation, so a row left over from another world is rejected rather than resolved against whatever took its place. Ordinary game code should ignore it. ##### `generation` ```nupp generation: uint32 ``` Engine-owned. Rejects a stale slot. Ordinary game code should ignore it. ## Functions ### `angularVelocity` _function_ ```nupp function angularVelocity(borrows world: ecs.World, entity: integer): number ``` Reads angular velocity in radians per second. Radians need no conversion, so this is the number the solver holds, unlike the linear velocity beside it. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity | #### Returns | Type | Description | | --- | --- | | `number` | angular velocity in radians per second, zero without a live body | ### `applyForce` _function_ ```nupp function applyForce(borrows world: ecs.World, entity: integer, x: number, y: number): nil ``` Applies a continuous force at the body's center. The solver clears forces at the end of every step, so holding a body up against gravity means calling this every fixed step rather than once. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity; a missing live body makes this a no-op | | `x` | `number` | the pixel-scaled force along x | | `y` | `number` | the pixel-scaled force along y, positive downward | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `applyForceAt` _function_ ```nupp function applyForceAt(borrows world: ecs.World, entity: integer, x: number, y: number, pointX: number, pointY: number): nil ``` Applies a continuous force at a world-space point. Cleared at the end of every step, like `applyForce`. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity; a missing live body makes this a no-op | | `x` | `number` | the pixel-scaled force along x | | `y` | `number` | the pixel-scaled force along y, positive downward | | `pointX` | `number` | the world-pixel application point along x | | `pointY` | `number` | the world-pixel application point along y | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `applyImpulse` _function_ ```nupp function applyImpulse(borrows world: ecs.World, entity: integer, x: number, y: number): nil ``` Pushes a body once, at its center of mass. An impulse rather than a force, so the effect does not depend on how long the step happened to be. The solver applies it at the start of the next fixed step and wakes the body, because a resting island would otherwise ignore it. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity; a missing live body makes this a no-op | | `x` | `number` | the pixel-scaled impulse along x | | `y` | `number` | the pixel-scaled impulse along y, positive downward | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `applyImpulseAt` _function_ ```nupp function applyImpulseAt(borrows world: ecs.World, entity: integer, x: number, y: number, pointX: number, pointY: number): nil ``` Pushes a body once at a world-space point, which spins it as well. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity; a missing live body makes this a no-op | | `x` | `number` | the pixel-scaled impulse along x | | `y` | `number` | the pixel-scaled impulse along y, positive downward | | `pointX` | `number` | the world-pixel impact position along x | | `pointY` | `number` | the world-pixel impact position along y | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `applyTorque` _function_ ```nupp function applyTorque(borrows world: ecs.World, entity: integer, torque: number): nil ``` Applies torque, cleared at the end of every step like the two forces. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity; a missing live body makes this a no-op | | `torque` | `number` | the solver's native torque units, where a positive value raises angular velocity | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `attach` _function_ ```nupp function attach(exclusive world: ecs.World, entity: integer, options: BodyOptions?): nil ``` Declares a body on an entity. Physics creates the body during the next fixed update, at which point the entity gains its `RigidBody` row. #### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive world` | `ecs.World` | a world with physics installed | | `entity` | `integer` | the entity that gains `Body` and `Collider`, and gains a default `Transform2D` and `Motion` at the next fixed step when it does not already carry them | | `options` | `BodyOptions?` | the initial body and collider settings, or nil for a dynamic eight-pixel box | #### Returns | Type | Description | | --- | --- | | `nil` | | #### Raises - when the world has no simulation, the body kind is unknown, or a capsule has no positive radius ### `attachCollider` _function_ ```nupp function attachCollider(exclusive world: ecs.World, entity: integer, body: integer, options: BodyOptions?): nil ``` Adds another collider to a declared body. The collider is its own entity, which makes each shape independently inspectable and mutable. Physics creates the shape at the first fixed step after its body is live, and an owner that never gains a [`Body`](tecs.physics.Body) leaves the shape declared and unrealized. #### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive world` | `ecs.World` | the world that owns both entities | | `entity` | `integer` | the secondary collider entity, which gains `Collider` and `ColliderOf` | | `body` | `integer` | an entity carrying `Body` | | `options` | `BodyOptions?` | the shape, material, filter and offset settings; body-level fields have no effect | #### Returns | Type | Description | | --- | --- | | `nil` | | #### Raises - when a capsule is requested without a positive radius ### `attachJoint` _function_ ```nupp function attachJoint(exclusive world: ecs.World, entity: integer, bodyA: integer, bodyB: integer, options: JointOptions?): nil ``` Declares a joint between two bodies. Physics creates the joint at the first fixed step after both bodies are live, so declaring the joint in the same frame as the bodies it constrains costs no extra frame. A target that never gains a [`Body`](tecs.physics.Body) leaves the joint declared and unrealized, which is deliberate: a spawn staged this frame is not published yet, so reading the target here would reject correct code. Destroying either body destroys the joint entity with it. #### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive world` | `ecs.World` | the world that owns all three entities | | `entity` | `integer` | the joint entity, which gains `Joint`, `JointBodyA` and `JointBodyB` | | `bodyA` | `integer` | the first constrained entity | | `bodyB` | `integer` | the second constrained entity | | `options` | `JointOptions?` | the joint kind, anchors, limits and motor settings | #### Returns | Type | Description | | --- | --- | | `nil` | | #### Raises - when the joint kind is unknown ### `detach` _function_ ```nupp function detach(exclusive world: ecs.World, entity: integer): nil ``` Removes a body's declaration. Physics destroys the body during the next fixed update. The entity's other physics components remain. #### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the entity that loses `Body` | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `hasBody` _function_ ```nupp function hasBody(borrows world: ecs.World, entity: integer): boolean ``` Reports whether the solver is still simulating a body for an entity. False for an entity whose `RigidBody` row came out of a snapshot into a simulation that never issued it, which is how a game tells "was simulating and is not any more" from "never had a body". #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | any entity | #### Returns | Type | Description | | --- | --- | | `boolean` | true only while the simulation holds a valid live body | ### `install` _function_ ```nupp function install(exclusive world: ecs.World, options: Options?): contract.Simulation ``` Installs a simulation and its fixed-step systems in a world. Every system this registers runs in the fixed phases, so a simulation advances by whole steps of the world's own timestep and replays the same way on every machine. #### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive world` | `ecs.World` | a world with no simulation yet | | `options` | `Options?` | the gravity, substep count, solver width, and an explicit simulation to drive instead of the default native one | #### Returns | Type | Description | | --- | --- | | `contract.Simulation` | the installed simulation, also available through `of` | #### Raises - when the world already has a simulation ### `isAwake` _function_ ```nupp function isAwake(borrows world: ecs.World, entity: integer): boolean ``` Reports whether the solver currently considers a body awake. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | any entity | #### Returns | Type | Description | | --- | --- | | `boolean` | true while the solver actively integrates the body, false when it sleeps or does not exist | ### `of` _function_ ```nupp function of(borrows world: ecs.World): contract.Simulation? ``` Returns the simulation installed in a world. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world to inspect | #### Returns | Type | Description | | --- | --- | | `contract.Simulation?` | the world's live simulation, or nil before `install` and after shutdown | ### `raycast` _function_ ```nupp function raycast(borrows world: ecs.World, x1: number, y1: number, x2: number, y2: number, options: QueryOptions?): RaycastHit? ``` Casts a segment and returns its nearest collider, in pixels. The cast tests one segment and ignores everything beyond its end, so a miss may mean the segment was too short. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | a world; one without physics returns nil | | `x1` | `number` | the segment start in world pixels along x | | `y1` | `number` | the segment start in world pixels along y | | `x2` | `number` | the segment end in world pixels along x | | `y2` | `number` | the segment end in world pixels along y | | `options` | `QueryOptions?` | the collision filters, or nil to test every shape including sensors | #### Returns | Type | Description | | --- | --- | | `RaycastHit?` | a fresh caller-owned nearest hit, or nil on a miss | ### `setAngularVelocity` _function_ ```nupp function setAngularVelocity(borrows world: ecs.World, entity: integer, omega: number): nil ``` Sets angular velocity in radians per second and wakes the body. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity; a missing live body makes this a no-op | | `omega` | `number` | angular velocity in radians per second | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `setAwake` _function_ ```nupp function setAwake(borrows world: ecs.World, entity: integer, awake: boolean): nil ``` Wakes or sleeps a body at the next fixed step. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity; a missing live body makes this a no-op | | `awake` | `boolean` | true to wake the body, false to sleep it | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `setVelocity` _function_ ```nupp function setVelocity(borrows world: ecs.World, entity: integer, vx: number, vy: number): nil ``` Sets a body's velocity, in pixels per second, and wakes it. Does nothing for an entity with no live body, which matches the zero `velocity` returns for one. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity | | `vx` | `number` | horizontal velocity in pixels per second | | `vy` | `number` | vertical velocity in pixels per second, positive downward | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `teleport` _function_ ```nupp function teleport(exclusive world: ecs.World, entity: integer, x: number, y: number, angle: number?): nil ``` Teleports a body and immediately updates its `Transform2D`. A move rather than a push: velocity is left exactly as it was, so a falling body carries on falling from wherever it lands. The solver receives the move at the next fixed step, while the transform changes now so the frame that requested the teleport already draws it there. #### Arguments | Name | Type | Description | | --- | --- | --- | | `exclusive world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity; a missing live body makes this a no-op, including its `Transform2D` | | `x` | `number` | the body's origin in world pixels along x | | `y` | `number` | the body's origin in world pixels along y, positive downward | | `angle` | `number?` | radians, or nil to keep the current `Transform2D.rotation` | #### Returns | Type | Description | | --- | --- | | `nil` | | ### `velocity` _function_ ```nupp function velocity(borrows world: ecs.World, entity: integer): number, number ``` Reads a body's velocity after the last completed fixed step, in pixels per second. Pixels match every other linear number this module accepts and returns. A caller that wants meters divides by `pixelsPerMeter`. This reports what the solver last computed. `Motion` stores velocity only at a pause or a save, and does not mirror live motion. #### Arguments | Name | Type | Description | | --- | --- | --- | | `borrows world` | `ecs.World` | the world holding the entity | | `entity` | `integer` | the target entity | #### Returns | Type | Description | | --- | --- | | `number` | horizontal velocity in pixels per second, zero without a live body | | `number` | vertical velocity in pixels per second, positive downward | ## Values ### `Body` _variable_ ```nupp const Body: ecs.ComponentDefinition ``` ### `Collider` _variable_ ```nupp const Collider: ecs.ComponentDefinition ``` ### `ColliderOf` _variable_ ```nupp const ColliderOf: ecs.Relationship ``` ### `ColliderShape` _variable_ ```nupp const ColliderShape: ecs.ComponentDefinition ``` ### `Joint` _variable_ ```nupp const Joint: ecs.ComponentDefinition ``` ### `JointBodyA` _variable_ ```nupp const JointBodyA: ecs.Relationship ``` ### `JointBodyB` _variable_ ```nupp const JointBodyB: ecs.Relationship ``` ### `JointLink` _variable_ ```nupp const JointLink: ecs.ComponentDefinition ``` ### `Motion` _variable_ ```nupp const Motion: ecs.ComponentDefinition ``` ### `PhysicsDisabled` _variable_ ```nupp const PhysicsDisabled: ecs.Component ``` ### `PhysicsHeld` _variable_ ```nupp const PhysicsHeld: ecs.Component ``` ### `pixelsPerMeter` _variable_ ```nupp const pixelsPerMeter: number ``` Reports the fixed conversion between public pixels and solver meters. The conversion keeps game coordinates in pixels and solver coordinates in meter-sized values. ### `RigidBody` _variable_ ```nupp const RigidBody: ecs.ComponentDefinition ```