tecs.physics.contract
The batched crossing between Tecs entities and a native rigid-body solver.
One fixed step is one crossing. A frame's systems queue commands through the queue* functions, step applies every queued command and advances the solver once, and the results are read back from buffers the simulation already holds. Nothing here calls a solver once per body, and nothing a solver owns ever calls back into Nupp: a Nupp function reached through a foreign function pointer cannot be entered from a compiled trace or from a thread the Nupp VM never created, so contacts, poses and assigned handles leave through buffers instead.
tecs.physics.rapier implements this over the tecs_physics Rust library. A test supplies its own implementation, which is what lets the entity-facing half of tecs.physics be exercised with no native library present.
Every length here is in solver meters and every angle is in radians. tecs.physics owns the pixel conversion and does not push it across this boundary.
A handle is a one-based index1 plus a generation. Zero index1 names nothing, which is what a snapshot load leaves behind and what a failed creation reports.
Module contents
Types
| Type | Kind | Description |
|---|---|---|
BodyCreate | record | One body creation, together with the primary collider it always carries. |
ColliderCreate | record | One secondary collider added to an existing or same-batch body. |
ColliderDef | record | One collider's geometry, material and collision filtering. |
ColliderUpdate | record | One in-place collider replacement, addressed by handle or by entity. |
EntityHandles | record | The body and collider handle owned by each entity a lookup asked about. |
JointCreate | record | One joint creation between two existing or same-batch bodies. |
Options | type | Options accepted when a simulation is created. |
Simulation | record | One live simulation, driven entirely through batched commands. |
Values
| Value | Kind | Description |
|---|---|---|
ACTION_FORCE | variable | Adds a center-of-mass force from a and b. |
ACTION_FORCE_AT | variable | Adds the force a, b at the world point c, d. |
ACTION_IMPULSE | variable | Adds a center-of-mass impulse from a and b. |
ACTION_IMPULSE_AT | variable | Adds the impulse a, b at the world point c, d. |
ACTION_SET_ANGULAR_VELOCITY | variable | Replaces angular velocity with a. |
ACTION_SET_AWAKE | variable | Wakes the body when a is nonzero and sleeps it otherwise. |
ACTION_SET_BODY_TYPE | variable | Replaces the body kind with a, one of the body-kind constants. |
ACTION_SET_ENABLED | variable | Enables the body when a is nonzero and disables it otherwise. |
ACTION_SET_TRANSFORM | variable | Teleports the body to a, b at angle c, leaving velocity alone. |
ACTION_SET_VELOCITY | variable | Replaces linear velocity with a, b. |
ACTION_TORQUE | variable | Adds the torque a. |
BODY_DYNAMIC | variable | A body the solver moves. |
BODY_KINEMATIC | variable | A body the game moves through velocity, which pushes dynamic bodies. |
BODY_STATIC | variable | A body the solver never moves. |
JOINT_FIXED | variable | A joint holding one relative pose between two bodies. |
JOINT_PRISMATIC | variable | A joint sharing one sliding axis, leaving translation along it free. |
JOINT_REVOLUTE | variable | A joint sharing one anchor point, leaving rotation about it free. |
SHAPE_BOX | variable | An axis-aligned box in the body frame. |
SHAPE_CAPSULE | variable | A vertical capsule. |
SHAPE_CIRCLE | variable | A circle. |
SHAPE_SEGMENT | variable | A centered segment from negative to positive half extents. |
Types#
BodyCreaterecord#
record BodyCreate
entity: integer
collider: ColliderDef
kind: integer
fixedRotation: boolean
isBullet: boolean
sleepEnabled: boolean
gravityScale: number
linearDamping: number
angularDamping: number
x: number
y: number
angle: number
vx: number
vy: number
omega: number
endOne body creation, together with the primary collider it always carries.
Fields
sleepEnabled#
sleepEnabled: booleanCaller-writable. Permits the body to sleep when it comes to rest.
ColliderCreaterecord#
record ColliderCreate
def: ColliderDef
bodyIndex1: integer
bodyGeneration: integer
bodyOrder: integer
endOne secondary collider added to an existing or same-batch body.
Fields
bodyIndex1#
bodyIndex1: integerCaller-writable. Names the owning body slot, one-based, when bodyOrder is zero.
bodyGeneration#
bodyGeneration: integerCaller-writable. Rejects a stale owning body with bodyIndex1.
bodyOrder#
bodyOrder: integerCaller-writable. Names the one-based order of the queueCreateBody call in this same batch that owns the shape, or zero when bodyIndex1 is authoritative. Queueing a shape onto a body the same batch creates is what keeps a compound body one crossing.
ColliderDefrecord#
record ColliderDef
entity: integer
shape: integer
sensor: boolean
halfWidth: number
halfHeight: number
radius: number
length: number
offsetX: number
offsetY: number
density: number
friction: number
restitution: number
categoryBits: integer
maskBits: integer
endOne collider's geometry, material and collision filtering.
Fields
entity#
entity: integerCaller-writable. Names the entity that owns this shape, which is what a contact event and an entity-addressed replacement both report.
length#
length: numberCaller-writable. Sets the distance between capsule end centers, in meters, excluding the caps.
ColliderUpdaterecord#
record ColliderUpdate
def: ColliderDef
index1: integer
generation: integer
bodyIndex1: integer
bodyGeneration: integer
endOne in-place collider replacement, addressed by handle or by entity.
Fields
index1#
index1: integerCaller-writable. Names the collider slot to replace, one-based, or zero to find the shape under bodyIndex1 whose entity is def.entity.
bodyIndex1#
bodyIndex1: integerCaller-writable. Names the owning body slot, one-based, used when index1 is zero.
bodyGeneration#
bodyGeneration: integerCaller-writable. Rejects a stale owning body with bodyIndex1.
EntityHandlesrecord#
record EntityHandles
bodyIndex1: {integer}
bodyGeneration: {integer}
colliderIndex1: {integer}
colliderGeneration: {integer}
jointIndex1: {integer}
jointGeneration: {integer}
endThe body and collider handle owned by each entity a lookup asked about.
Each array holds one entry per requested entity, in the requested order, and a zero index1 means the simulation has nothing for that entity.
Fields
colliderGeneration#
colliderGeneration: {integer}Read-only. Holds each entity's collider generation.
JointCreaterecord#
record JointCreate
entity: integer
kind: integer
bodyAIndex1: integer
bodyAGeneration: integer
bodyAOrder: integer
bodyBIndex1: integer
bodyBGeneration: integer
bodyBOrder: integer
anchorAX: number
anchorAY: number
anchorBX: number
anchorBY: number
axisX: number
axisY: number
limitsEnabled: boolean
limitMin: number
limitMax: number
motorEnabled: boolean
motorTargetVelocity: number
motorMaxForce: number
contactsEnabled: boolean
endOne joint creation between two existing or same-batch bodies.
Fields
bodyAIndex1#
bodyAIndex1: integerCaller-writable. Names the first body slot, one-based, when bodyAOrder is zero.
bodyAGeneration#
bodyAGeneration: integerCaller-writable. Rejects a stale first body with bodyAIndex1.
bodyAOrder#
bodyAOrder: integerCaller-writable. Names the one-based queueCreateBody order in this batch that supplies the first body, or zero.
bodyBIndex1#
bodyBIndex1: integerCaller-writable. Names the second body slot, one-based, when bodyBOrder is zero.
bodyBGeneration#
bodyBGeneration: integerCaller-writable. Rejects a stale second body with bodyBIndex1.
bodyBOrder#
bodyBOrder: integerCaller-writable. Names the one-based queueCreateBody order in this batch that supplies the second body, or zero.
anchorAX#
anchorAX: numberCaller-writable. Anchors the joint in the first body's frame along x, in meters.
axisX#
axisX: numberCaller-writable. Sets the prismatic sliding axis along x. The solver normalizes it, and the other joint kinds ignore it.
limitMin#
limitMin: numberCaller-writable. Sets the lower limit, in radians for a revolute joint and meters for a prismatic one.
motorTargetVelocity#
motorTargetVelocity: numberCaller-writable. Sets the motor's target velocity, in radians or meters per second.
motorMaxForce#
motorMaxForce: numberCaller-writable. Bounds the force or torque the motor may apply.
contactsEnabled#
contactsEnabled: booleanCaller-writable. Lets the two joined bodies still collide.
Optionstype#
type Options = {
gravityX: number?,
gravityY: number?,
subStepCount: integer?,
workerCount: integer?
}Options accepted when a simulation is created.
Simulationrecord#
record Simulation
id: integer
subStepCount: integer
workerCount: integer
queueCreateBody: function(command: BodyCreate): integer
queueCreateCollider: function(command: ColliderCreate): integer
queueCreateJoint: function(command: JointCreate): integer
queueUpdateBody: function(
index1: integer,
generation: integer,
kind: integer,
fixedRotation: boolean,
isBullet: boolean,
sleepEnabled: boolean,
gravityScale: number,
linearDamping: number,
angularDamping: number
): nil
queueUpdateCollider: function(command: ColliderUpdate): integer
queueAction: function(
index1: integer,
generation: integer,
action: integer,
a: number,
b: number,
c: number,
d: number
): nil
queueDestroyBody: function(index1: integer, generation: integer): nil
queueDestroyCollider: function(index1: integer, generation: integer): nil
queueDestroyJoint: function(index1: integer, generation: integer): nil
step: function(dt: number): nil
createdBody: function(order: integer): (integer, integer, integer, integer)
createdCollider: function(order: integer): (integer, integer)
createdJoint: function(order: integer): (integer, integer)
updatedCollider: function(order: integer): (integer, integer)
isLive: function(index1: integer, generation: integer): boolean
isAwake: function(index1: integer, generation: integer): boolean
poseOf: function(index1: integer, generation: integer): (number?, number, number, number)
velocityOf: function(index1: integer, generation: integer): (number, number, number)
eventCount: function(): integer
eventAt: function(index: integer): (integer, integer, boolean, boolean)
raycast: function(
x1: number,
y1: number,
x2: number,
y2: number,
categoryBits: integer,
maskBits: integer
): (integer?, number, number, number, number, number)
resolveEntities: function(entities: {integer}): EntityHandles
save: function(): (string?, string?)
load: function(bytes: string): (boolean, string?)
destroy: function(): nil
endOne live simulation, driven entirely through batched commands.
Every field is a closure over one simulation's private state, so an implementation is a value rather than a subclass and a test may supply its own without a native library.
Methods
queueCreateBody#
queueCreateBody: function(command: BodyCreate): integerQueues one body creation and returns its one-based order in this batch, which createdBody and a same-batch bodyOrder both use.
Arguments
| Name | Type | Description |
|---|---|---|
command | BodyCreate |
Returns
| Type | Description |
|---|---|
integer |
queueCreateCollider#
queueCreateCollider: function(command: ColliderCreate): integerQueues one secondary collider and returns its one-based order.
Arguments
| Name | Type | Description |
|---|---|---|
command | ColliderCreate |
Returns
| Type | Description |
|---|---|
integer |
queueCreateJoint#
queueCreateJoint: function(command: JointCreate): integerQueues one joint and returns its one-based order.
Arguments
| Name | Type | Description |
|---|---|---|
command | JointCreate |
Returns
| Type | Description |
|---|---|
integer |
queueUpdateBody#
queueUpdateBody: function(
index1: integer,
generation: integer,
kind: integer,
fixedRotation: boolean,
isBullet: boolean,
sleepEnabled: boolean,
gravityScale: number,
linearDamping: number,
angularDamping: number
): nilQueues one body redeclaration, which never moves the body and never changes its owning entity.
The arguments are positional because a redeclaration runs once per dirty body row, and a record per row is an allocation in a loop whose length is the game's business.
Arguments
| Name | Type | Description |
|---|---|---|
index1 | integer | the one-based target body slot |
generation | integer | the generation that must match |
kind | integer | one of the body-kind constants |
fixedRotation | boolean | whether the body's angle is locked |
isBullet | boolean | whether the body sweeps its motion |
sleepEnabled | boolean | whether the body may sleep at rest |
gravityScale | number | the body's own gravity multiplier |
linearDamping | number | the linear velocity damping term |
angularDamping | number | the angular velocity damping term |
Returns
| Type | Description |
|---|---|
nil |
queueUpdateCollider#
queueUpdateCollider: function(command: ColliderUpdate): integerQueues one collider replacement and returns its one-based order, which updatedCollider uses to read the replacement's handle.
Arguments
| Name | Type | Description |
|---|---|---|
command | ColliderUpdate |
Returns
| Type | Description |
|---|---|
integer |
queueAction#
queueAction: function(
index1: integer,
generation: integer,
action: integer,
a: number,
b: number,
c: number,
d: number
): nilQueues one mutation of a live body.
Actions apply at the start of the step that follows them, in the order they were queued, and reads report the state after the last completed step. A queued impulse is therefore not visible until that step runs. The arguments are positional because a game applies actions in its own loops.
Arguments
| Name | Type | Description |
|---|---|---|
index1 | integer | the one-based target body slot |
generation | integer | the generation that must match |
action | integer | one of the action constants |
a | number | the action's first operand |
b | number | the action's second operand |
c | number | the action's third operand |
d | number | the action's fourth operand |
Returns
| Type | Description |
|---|---|
nil |
queueDestroyBody#
queueDestroyBody: function(index1: integer, generation: integer): nilQueues one body destruction, which also removes its colliders and joints. A slot the simulation no longer holds is ignored.
Arguments
| Name | Type | Description |
|---|---|---|
index1 | integer | |
generation | integer |
Returns
| Type | Description |
|---|---|
nil |
queueDestroyCollider#
queueDestroyCollider: function(index1: integer, generation: integer): nilQueues one collider destruction, ignoring a slot already gone.
Arguments
| Name | Type | Description |
|---|---|---|
index1 | integer | |
generation | integer |
Returns
| Type | Description |
|---|---|
nil |
queueDestroyJoint#
queueDestroyJoint: function(index1: integer, generation: integer): nilQueues one joint destruction, ignoring a slot already gone.
Arguments
| Name | Type | Description |
|---|---|---|
index1 | integer | |
generation | integer |
Returns
| Type | Description |
|---|---|
nil |
step#
step: function(dt: number): nilApplies every queued command and advances the simulation once.
Commands apply in a fixed order that does not depend on the order systems discovered them: joint, collider and body destruction, then body, collider and joint creation, then body redeclaration, collider replacement, and queued actions. The queues are empty afterwards.
Arguments
| Name | Type | Description |
|---|---|---|
dt | number | the interval to advance, in seconds |
Returns
| Type | Description |
|---|---|
nil |
Raises
when the simulation cannot apply the batch
createdBody#
createdBody: function(order: integer): (integer, integer, integer, integer)Returns the body and collider assigned to one queued creation.
Arguments
| Name | Type | Description |
|---|---|---|
order | integer | the one-based order |
Returns
| Type | Description |
|---|---|
integer | the body slot, one-based and zero when creation failed, the body generation, the primary collider slot, and its generation |
integer | |
integer | |
integer |
createdCollider#
createdCollider: function(order: integer): (integer, integer)Returns the collider assigned to one queued secondary creation.
Arguments
| Name | Type | Description |
|---|---|---|
order | integer | the one-based order |
Returns
| Type | Description |
|---|---|
integer | the collider slot, one-based and zero on failure, and its generation |
integer |
createdJoint#
createdJoint: function(order: integer): (integer, integer)Returns the joint assigned to one queued creation.
Arguments
| Name | Type | Description |
|---|---|---|
order | integer | the one-based order |
Returns
| Type | Description |
|---|---|
integer | the joint slot, one-based and zero on failure, and its generation |
integer |
updatedCollider#
updatedCollider: function(order: integer): (integer, integer)Returns the collider a queued replacement produced.
Arguments
| Name | Type | Description |
|---|---|---|
order | integer | the one-based order |
Returns
| Type | Description |
|---|---|
integer | the collider slot, one-based and zero on failure, and its generation |
integer |
isLive#
isLive: function(index1: integer, generation: integer): booleanReports whether the simulation still holds this body.
Arguments
| Name | Type | Description |
|---|---|---|
index1 | integer | |
generation | integer |
Returns
| Type | Description |
|---|---|
boolean |
isAwake#
isAwake: function(index1: integer, generation: integer): booleanReports whether the solver is still integrating this body.
Arguments
| Name | Type | Description |
|---|---|---|
index1 | integer | |
generation | integer |
Returns
| Type | Description |
|---|---|
boolean |
poseOf#
poseOf: function(index1: integer, generation: integer): (number?, number, number, number)Returns a body's pose only when the last step moved it.
Arguments
| Name | Type | Description |
|---|---|---|
index1 | integer | the one-based body slot |
generation | integer | the generation that must match |
Returns
| Type | Description |
|---|---|
number? | nil when the body is gone or did not move, otherwise x in meters, y in meters, the cosine of its angle, and the sine |
number | |
number | |
number |
velocityOf#
velocityOf: function(index1: integer, generation: integer): (number, number, number)Returns a body's velocity after the last completed step.
Arguments
| Name | Type | Description |
|---|---|---|
index1 | integer | the one-based body slot |
generation | integer | the generation that must match |
Returns
| Type | Description |
|---|---|
number | linear velocity along x and y in meters per second, and angular velocity in radians per second, all zero when the body is gone |
number | |
number |
eventCount#
eventCount: function(): integerReports how many contact and sensor transitions the last step queued.
Returns
| Type | Description |
|---|---|
integer |
eventAt#
eventAt: function(index: integer): (integer, integer, boolean, boolean)Returns one queued transition.
Arguments
| Name | Type | Description |
|---|---|---|
index | integer | the one-based position within the last step's events |
Returns
| Type | Description |
|---|---|
integer | the first entity, the second entity, whether the pair began touching rather than stopped, and whether the pair involved a sensor. A sensor transition names the sensor first. |
integer | |
boolean | |
boolean |
raycast#
raycast: function(
x1: number,
y1: number,
x2: number,
y2: number,
categoryBits: integer,
maskBits: integer
): (integer?, number, number, number, number, number)Casts a finite segment and returns its nearest hit.
Arguments
| Name | Type | Description |
|---|---|---|
x1 | number | the segment start along x, in meters |
y1 | number | the segment start along y, in meters |
x2 | number | the segment end along x, in meters |
y2 | number | the segment end along y, in meters |
categoryBits | integer | the ray's collision categories |
maskBits | integer | the categories the ray may hit |
Returns
| Type | Description |
|---|---|
integer? | nil on a miss, otherwise the owning entity, the contact point x and y in meters, the outward unit normal x and y, and the fraction along the segment |
number | |
number | |
number | |
number | |
number |
resolveEntities#
resolveEntities: function(entities: {integer}): EntityHandlesAnswers the handles the simulation holds for a list of entities.
A snapshot load asks about every restored entity at once, so the cost stays linear in the simulation rather than quadratic.
Arguments
| Name | Type | Description |
|---|---|---|
entities | {integer} | the entity ids to look up |
Returns
| Type | Description |
|---|---|
EntityHandles | one caller-owned record of parallel result arrays |
save#
save: function(): (string?, string?)Serializes the complete simulation state.
Returns
| Type | Description |
|---|---|
string? | the bytes, or nil and a reason when the state cannot be saved |
string? |
load#
load: function(bytes: string): (boolean, string?)Replaces the simulation state with previously saved bytes.
Slots and generations survive, so a handle a save recorded still addresses the same body afterwards.
Arguments
| Name | Type | Description |
|---|---|---|
bytes | string | the value a previous |
Returns
| Type | Description |
|---|---|
boolean | whether the state was restored, and a reason when it was not |
string? |
destroy#
destroy: function(): nilReleases the simulation and everything it owns.
Calling it twice is harmless. Every other function is undefined afterwards.
Returns
| Type | Description |
|---|---|
nil |
Fields
id#
id: integerRead-only. Identifies this simulation among the process's live ones, which is what a stored handle carries so a row left over from another simulation is rejected rather than resolved against whatever took its place.
subStepCount#
subStepCount: integerRead-only. Reports how many substeps each step divides its interval into.
Values#
ACTION_FORCEvariable#
const ACTION_FORCE: integerAdds a center-of-mass force from a and b.
ACTION_FORCE_ATvariable#
const ACTION_FORCE_AT: integerAdds the force a, b at the world point c, d.
ACTION_IMPULSEvariable#
const ACTION_IMPULSE: integerAdds a center-of-mass impulse from a and b.
ACTION_IMPULSE_ATvariable#
const ACTION_IMPULSE_AT: integerAdds the impulse a, b at the world point c, d.
ACTION_SET_ANGULAR_VELOCITYvariable#
const ACTION_SET_ANGULAR_VELOCITY: integerReplaces angular velocity with a.
ACTION_SET_AWAKEvariable#
const ACTION_SET_AWAKE: integerWakes the body when a is nonzero and sleeps it otherwise.
ACTION_SET_BODY_TYPEvariable#
const ACTION_SET_BODY_TYPE: integerReplaces the body kind with a, one of the body-kind constants.
ACTION_SET_ENABLEDvariable#
const ACTION_SET_ENABLED: integerEnables the body when a is nonzero and disables it otherwise.
ACTION_SET_TRANSFORMvariable#
const ACTION_SET_TRANSFORM: integerTeleports the body to a, b at angle c, leaving velocity alone.
ACTION_SET_VELOCITYvariable#
const ACTION_SET_VELOCITY: integerReplaces linear velocity with a, b.
ACTION_TORQUEvariable#
const ACTION_TORQUE: integerAdds the torque a.
BODY_DYNAMICvariable#
const BODY_DYNAMIC: integerA body the solver moves.
BODY_KINEMATICvariable#
const BODY_KINEMATIC: integerA body the game moves through velocity, which pushes dynamic bodies.
BODY_STATICvariable#
const BODY_STATIC: integerA body the solver never moves.
JOINT_FIXEDvariable#
const JOINT_FIXED: integerA joint holding one relative pose between two bodies.
JOINT_PRISMATICvariable#
const JOINT_PRISMATIC: integerA joint sharing one sliding axis, leaving translation along it free.
JOINT_REVOLUTEvariable#
const JOINT_REVOLUTE: integerA joint sharing one anchor point, leaving rotation about it free.
SHAPE_BOXvariable#
const SHAPE_BOX: integerAn axis-aligned box in the body frame.
SHAPE_CAPSULEvariable#
const SHAPE_CAPSULE: integerA vertical capsule.
SHAPE_CIRCLEvariable#
const SHAPE_CIRCLE: integerA circle.
SHAPE_SEGMENTvariable#
const SHAPE_SEGMENT: integerA centered segment from negative to positive half extents.