Skip to content

Joints

A joint holds two entities at a distance from each other. It is an entity of its own with one joint component, which names its two ends in bodyA and bodyB. It is not a component on a body, so a body can have any number of joints, and the joint entity can carry what belongs to the rope itself, such as its sprite.

PhysicsPlugin installs JointsPlugin. To install it alone, import it from @shell/joints.

DistanceJoint keeps its ends no nearer than minLength and no farther than maxLength. Between the two it does nothing.

import { DistanceJoint } from '@shell/physics';
// A rope: slack until its ends are 170 apart.
commands.spawn().root.addComponent(DistanceJoint, { bodyA: player, bodyB: ball, maxLength: 170 });
// A rod: both lengths the same.
commands
.spawn()
.root.addComponent(DistanceJoint, { bodyA: a, bodyB: b, minLength: 40, maxLength: 40 });
FieldTypeDefaultDescription
bodyAEntity | undefinedundefinedOne end
bodyBEntity | undefinedundefinedThe other end
minLengthnumber0The ends are pushed apart when nearer than this
maxLengthnumber100The ends are pulled together when farther apart than this

A chain is rods in a row, each joint naming two neighbouring links. A maxLength under minLength counts as minLength.

When a limit is passed, the ends are moved back to it and the speed that would carry them further past it is taken away. A taut rope does not stop its ends from coming together.

SpringJoint draws its ends toward length, softly. It pushes when they are nearer and pulls when they are farther.

import { SpringJoint } from '@shell/physics';
commands.spawn().root.addComponent(SpringJoint, {
bodyA: hook,
bodyB: lamp,
length: 80,
frequency: 2,
dampingRatio: 0.3,
});
FieldTypeDefaultDescription
bodyAEntity | undefinedundefinedOne end
bodyBEntity | undefinedundefinedThe other end
lengthnumber100The distance the spring rests at
frequencynumber2Bounces per second. 0 turns the spring off
dampingRationumber0.5How fast it settles: 0 never, 1 without a bounce, more is slower

A spring is tuned by how it feels, not by a stiffness: the same frequency bounces as often whatever the masses of its ends. It is solved over the whole step at once, so no value makes it blow up.

An end is any entity with a Position. A joint moves an end only when it is a dynamic RigidBody with a Velocity, the rule the collision resolver has. Every other end is fixed: a static or kinematic body, or an entity with nothing but a Position.

// A lamp on a rope from the ceiling. The hook is only a place.
const hook = commands.spawn().root;
hook.addComponent(Position, { x: 400, y: 0, z: 0 });
commands.spawn().root.addComponent(DistanceJoint, { bodyA: hook, bodyB: lamp, maxLength: 120 });

Between two ends that can both move, the pull is shared by mass: the heavier end moves less. There is no share to set on the joint. To have a ball drag a player less, make the player heavier.

A joint with an end that is empty or despawned does nothing, and stays. Removing it is the game’s to do.

Both systems run in PhysicsSet.Joints, after positions are integrated and before collisions are detected, springs first. A collision therefore has the last word: a rope may be a hair long for a step, but a body is not left inside a wall.

Distance joints are solved one after another, in a number of passes each fixed step:

world.get('physicsJoints').iterations = 8;
ResourceFieldDefaultDescription
physicsJointsiterations4Passes over the distance joints each fixed step

A chain wants about one pass for each link. With fewer, a long chain stretches.

  • No joint holds an angle. There is no hinge and no weld, because collisions here have no rotation.
  • An end is tied at its Position. There is no offset on a body to tie it at.
  • Ends are read by Position, as colliders are, so an end under a parent that is moved is off by the parent’s place.
  • A joint does not draw itself. Debug Visualization shows a line for each; a sprite stretched between the ends is a system a game writes.
  • A spring with a hard limit is two joints between the same ends.