Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ryanhcode/sable/llms.txt

Use this file to discover all available pages before exploring further.

Sable drives sub-level motion through an abstracted PhysicsPipeline interface backed by Rapier, a Rust physics engine. Physics runs server-side only, at 20 ticks per second with a configurable number of substeps per tick. The pipeline treats each sub-level as a rigid body with mass and inertia derived from its blocks, and exposes a clean Java API for applying forces, reading velocities, teleporting bodies, and adding constraints between them.
The Rapier integration is implemented in Rust. Building Sable from source requires Docker to compile the native library. Pre-built binaries are distributed with release artifacts, so you only need Docker if you are modifying the physics backend itself. See the installation guide for details.

The PhysicsPipeline interface

PhysicsPipeline is the entry point for all physics operations. You do not instantiate it directly; Sable creates and attaches an implementation to each ServerSubLevelContainer at world load time. Access it through the container:
ServerSubLevelContainer container = SubLevelContainer.getContainer(serverLevel);
PhysicsPipeline pipeline = container.physicsSystem().getPipeline();
Sub-levels are added to and removed from the pipeline automatically when they are allocated or removed. You can also add kinematic contraptions and rope or box physics objects through the same interface.

Tick lifecycle

Each game tick, the pipeline runs through three phases:
  1. prePhysicsTicks() — prepares the simulation state for the current tick.
  2. physicsTick(double timeStep) — advances the simulation by one substep. This is called multiple times per tick; each call receives a timeStep of 1.0 / 20.0 / substeps seconds.
  3. postPhysicsTicks() — finalizes the tick and makes the updated poses available for reading.
A separate tick() call handles data tracking and other logic that should run even when physics is paused.

Reading pose

After postPhysicsTicks(), you can read the current pose of any sub-level’s physics body:
Pose3d pose = new Pose3d();
pipeline.readPose(subLevel, pose);
// pose.position() and pose.orientation() are now up to date

Teleportation

To move a body instantly without simulation, use teleport:
pipeline.teleport(subLevel, position, orientation);
This is typically called right after assembly to place a newly created sub-level at its initial world position.

Applying forces

applyImpulse

Applies a force at a specific world position on the body. The resulting torque is calculated automatically from the offset between the application point and the center of mass.
pipeline.applyImpulse(body, position, force);
force is in Newtons [N].

applyLinearAndAngularImpulse

Applies a linear force and a torque directly to the body’s center of mass. Pass wakeUp = true to resume a sleeping body.
pipeline.applyLinearAndAngularImpulse(
    body, force, torque, true
);
torque is in Newton-meters [Nm].

Reading velocity

Vector3d linear  = pipeline.getLinearVelocity(body, new Vector3d());  // m/s
Vector3d angular = pipeline.getAngularVelocity(body, new Vector3d()); // rad/s
Both methods write into a provided destination vector and return it. You can also add velocity directly with addLinearAndAngularVelocity, or zero everything out with resetVelocity.

Waking up bodies

Rapier puts bodies to sleep when they stop moving to save CPU. If you change a body’s environment (e.g. by adding or removing blocks) without applying a force, call wakeUp to resume simulation:
pipeline.wakeUp(body);

Constraints

Constraints create joints between two sub-levels (or between a sub-level and the world). Pass null for either sub-level argument to pin the other to the world:
PhysicsConstraintHandle handle = pipeline.addConstraint(
    sublevelA,
    sublevelB,
    configuration
);
The configuration parameter is a PhysicsConstraintConfiguration that describes the joint type and its parameters. Hold the returned PhysicsConstraintHandle to remove or update the constraint later.

Block physics properties

The mass, inertia tensor, volume, restitution, and friction of a sub-level’s rigid body are derived from the blocks it contains. Sable reads physics properties from each block and accumulates them into the body’s MassData. You can configure per-block physics properties using the block physics properties data-driven system.
When the block composition of a sub-level changes (blocks added or removed), Sable automatically calls onStatsChanged on the pipeline to re-upload the updated mass properties.

Build docs developers (and LLMs) love