Skip to content

Physics

Rally simulates physics on the game server: falling crates, rolling balls, swinging doors and spinning windmills all move there, and every player sees the result. This page covers how parts behave physically, how to move them, how to join them with constraints, and how to ask the world questions with raycasts.

Physics uses studs for distance and seconds for time. Gravity pulls down at workspace.gravity, 196.2 studs per second squared by default. You can change it in Studio (select Workspace in the Explorer) or from a script at any time.

Every part starts anchored: physics never moves it. That’s what you want for floors, walls, platforms and scenery.

Turn anchored off to hand a part to the physics simulation. An unanchored part falls, rolls, bounces, gets knocked over and lands on other parts. In Studio, select the part and clear anchored under Physics in Properties. From a script, set it like any other property:

ServerScriptService/CrateRain.server.ts
import { Color3, Part, task, Vector3, workspace } from '@rally/sdk';
// A crate falls from the sky every 2 seconds and disappears 20 seconds later.
task.spawn(async () => {
for (;;) {
const crate = new Part();
crate.name = 'Crate';
crate.size = new Vector3(3, 3, 3);
crate.position = new Vector3(Math.random() * 40 - 20, 60, Math.random() * 40 - 20);
crate.rotation = new Vector3(0, Math.random() * 360, 0);
crate.color = Color3.fromHex('#b5835a');
crate.material = 'wood';
crate.anchored = false; // physics takes over
crate.parent = workspace;
task.delay(20, () => crate.destroy());
await task.wait(2);
}
});

Unanchored parts that fall below workspace.fallenPartsDestroyHeight (-500 by default) are destroyed for you, and characters that fall below it die.

You can still move an anchored part from a script: set its position or rotation, or animate it with a tween or an Animation. It goes straight to each new position, which is right for doors, elevators and moving platforms.

  • Characters standing on a moving part ride along with it.
  • Unanchored parts resting on it don’t: a platform sliding sideways slides out from under a crate. A rising platform does push things up.
  • Moving an anchored part into another anchored part doesn’t fire touched. See Touch events.

A Part has a shape, and it collides as that shape:

shape Collides as
box (default) A box of size.
sphere A ball whose diameter is size.x.
cylinder An upright cylinder size.y tall. Its diameter is the smaller of size.x and size.z.
capsule An upright capsule size.y tall, with rounded ends. Its radius is half the smaller of size.x and size.z.

A MeshPart collides as a box of its size, whatever its model looks like. For an oddly shaped model, turn off its canCollide and add invisible anchored parts (transparency 1) where you want solid walls.

Parts inside a Model are still separate bodies. To make them move as one, join them with constraints.

Two properties decide how a part meets the world:

Property Default What it does
canCollide true Parts and characters bump into it. Turn it off for ghosts: zones, pickups, decorations you can walk through.
canTouch true Fires touched and touchEnded. Turn it off for scenery nobody needs to detect.

A part with canCollide off still fires touch events, which is how checkpoints, coins and trigger zones work.

touched fires on the server when something starts touching a part, and touchEnded when it stops. They fire once per contact, not every frame: a character standing on a pad fires touched once.

What you get as other depends on what touched the part:

  • A character touching any part with canTouch on, including parts with canCollide off, fires with the Character. Use Players.getPlayerFromCharacter(other) to find its player.
  • Another part fires when at least one of the two is unanchored. Both parts fire, each with the other one. Two anchored parts never touch, even if a script moves one into the other.
  • Terrain: a part that lands on terrain fires with the Terrain object.

Touch events never fire on players’ devices: connect them in Scripts, not LocalScripts.

This script heals every character standing in a zone, once a second. It keeps a set of who’s inside by listening to both events:

Workspace/HealZone/Heal.server.ts
import { BasePart, Character, script, task } from '@rally/sdk';
const zone = script.parent;
if (!(zone instanceof BasePart)) throw new Error('Put this script inside a part');
zone.canCollide = false; // walk through it, not into it
zone.transparency = 0.6;
const inside = new Set<Character>();
zone.touched.connect((other) => {
if (other instanceof Character) inside.add(other);
});
zone.touchEnded.connect((other) => {
if (other instanceof Character) inside.delete(other);
});
task.spawn(async () => {
for (;;) {
for (const character of inside) {
if (character.destroyed || character.health <= 0) inside.delete(character);
else character.health = Math.min(character.maxHealth, character.health + 10);
}
await task.wait(1);
}
});
Property Default Range What it does
density 1 0.01 to 100 Mass per volume. Heavier parts push lighter ones around and are harder to move.
friction 0.3 0 to 2 How much the part grips what it slides on, and what slides on it.
restitution 0 0 to 1 Bounciness. 0 thuds; 1 bounces as high as it fell.

Values outside the range are clamped. When two parts touch, their friction and their restitution are averaged. A ball with restitution 1 dropped on an ordinary floor (0) bounces like 0.5, which only reaches about a quarter of the height it fell from; for a trampoline, make the floor bouncy too.

material only changes how a part looks: a part with the ice material is exactly as grippy as its friction says. And these properties only affect parts: characters walk at their walkSpeed on any surface, and don’t bounce.

Changing size, shape, anchored, canCollide, density, friction or restitution rebuilds the part’s body in the simulation. That’s fine now and then, but avoid doing it every frame.

Unanchored parts have two motion properties you can read and set:

Property Units What it does
velocity studs per second How fast it moves and in which direction. Assigning it launches the part.
angularVelocity radians per second, around each axis How fast it spins. Assigning it spins the part.

applyImpulse(impulse) gives an unanchored part a push. The push changes its velocity by the impulse divided by the part’s mass, so the same push moves a light part more than a heavy one. Use it for knockback; to launch something at an exact speed, set velocity instead.

All of these only act on the server, on unanchored parts. Setting position or rotation on an unanchored part teleports it, and physics carries on from there.

This cannon fires a bouncy ball at a target part every 3 seconds:

Workspace/Cannon/Fire.server.ts
import { BasePart, Color3, getChild, Part, script, task, Vector3, workspace } from '@rally/sdk';
const cannon = script.parent;
if (!(cannon instanceof BasePart)) throw new Error('Put this script inside a part');
const target = getChild(workspace, 'Target', BasePart);
task.spawn(async () => {
for (;;) {
const aim = target.position.sub(cannon.position).unit;
const ball = new Part();
ball.shape = 'sphere';
ball.size = new Vector3(2, 2, 2);
ball.color = Color3.fromHex('#ff6b35');
ball.restitution = 0.7;
ball.anchored = false;
ball.position = cannon.position.add(aim.mul(3)); // start just outside the cannon
ball.velocity = aim.mul(80).add(new Vector3(0, 20, 0)); // fast, and a little upward
ball.parent = workspace;
task.delay(8, () => ball.destroy());
await task.wait(3);
}
});

Characters aren’t physics bodies. Each player moves their own character on their device, so forces, impulses and collisions don’t push characters around. To move one from the server:

  • call character.teleport(position) (or assign character.position);
  • change walkSpeed and jumpPower to make it faster or jump higher.

This pad makes characters jump much higher while they stand on it:

Workspace/JumpPad/Boost.server.ts
import { BasePart, Character, script, StarterPlayer, task } from '@rally/sdk';
const pad = script.parent;
if (!(pad instanceof BasePart)) throw new Error('Put this script inside a part');
const BOOST = 120; // jump power on the pad (the default is 50)
pad.touched.connect((other) => {
if (other instanceof Character && other.health > 0) other.jumpPower = BOOST;
});
pad.touchEnded.connect((other) => {
if (!(other instanceof Character)) return;
// Keep the boost a moment, so the jump that takes you off the pad still counts.
task.delay(0.4, () => {
if (!other.destroyed && other.jumpPower === BOOST)
other.jumpPower = StarterPlayer.characterJumpPower;
});
});

More about characters in Players and characters.

Constraints join two parts with physics. Each one has part0, part1 and enabled:

Constraint What it does Use it for
WeldConstraint Glues the two parts together as they are, so they move as one. Building machines out of several parts, locking something in place.
HingeConstraint Lets part1 turn around an axis, with an optional motor and angle limits. Doors, wheels, fans, trapdoors, windmills.
SpringConstraint Pulls or pushes the two parts toward a rest length. Bouncy pads, suspension, wobbly decorations.
RopeConstraint Lets the two parts move freely, but never farther apart than length. Swings, hanging lamps, wrecking balls, tethers.

An anchored part holds still, so joining an unanchored part to an anchored one pins it to the world. Joining two anchored parts does nothing. Constraints act on the server only; players see the parts move.

  1. Select the two parts. The first one you select becomes part0.

  2. Choose Insert > Constraints and the kind of constraint. Studio puts it next to the parts and connects it to both.

  3. Set the rest in Properties: pivots, axis, motor, limits, lengths.

You can also insert a constraint on its own and pick part0 and part1 from the lists in Properties.

Hinges, springs and ropes attach at a pivot on each part: pivot0 on part0 and pivot1 on part1. A pivot is an offset from the part’s center, in studs, measured along the part’s own (rotated) axes. (0, 0, 0) is the center; (0, 1, 0) is 1 stud above the center.

For a hinge, both pivots name the same point, seen from each part. A door 6 studs wide hinged on its left edge to a post on its left has pivot1 = (-3, 0, 0) (the door’s left edge) and a pivot0 at the post’s matching face. The hinge’s axis is the line it turns around, in part0’s axes: (0, 1, 0) is an ordinary upright door.

Property Default What it does
motorEnabled false Drive the hinge at angularVelocity.
angularVelocity 0 Motor speed in radians per second. Positive turns counter-clockwise around axis.
motorMaxTorque 100000 The strongest twist the motor uses to keep its speed.
limitsEnabled false Keep the angle between lowerAngle and upperAngle.
lowerAngle, upperAngle -90, 90 Degrees, from the pose the parts were in when the hinge was set up.

A motorized windmill, built entirely from a script:

ServerScriptService/Windmill.server.ts
import { Color3, HingeConstraint, Model, Part, Vector3, workspace } from '@rally/sdk';
const mill = new Model();
mill.name = 'Windmill';
const tower = new Part(); // anchored
tower.name = 'Tower';
tower.size = new Vector3(2, 16, 2);
tower.position = new Vector3(0, 8, -20);
tower.parent = mill;
const blades = new Part();
blades.name = 'Blades';
blades.size = new Vector3(14, 1.5, 0.5);
blades.position = new Vector3(0, 15, -18.75); // flat against the tower's front face
blades.color = Color3.fromHex('#f4f1de');
blades.anchored = false;
blades.parent = mill;
const axle = new HingeConstraint();
axle.part0 = tower;
axle.part1 = blades;
axle.pivot0 = new Vector3(0, 7, 1); // the tower's front face, near the top
axle.pivot1 = new Vector3(0, 0, -0.25); // the back of the blades, at their center
axle.axis = new Vector3(0, 0, 1); // turn around Z
axle.motorEnabled = true;
axle.angularVelocity = 1.5;
axle.parent = mill;
mill.parent = workspace;
Constraint Property Default What it does
SpringConstraint restLength 4 The relaxed length, in studs.
stiffness 100 Force per stud of stretch. Heavier parts need stiffer springs.
damping 1 Slows the bouncing. 0 bounces forever.
RopeConstraint length 10 The farthest apart the two pivots can get, in studs.

A wrecking ball on a rope, let go from the side so it swings:

ServerScriptService/WreckingBall.server.ts
import { Color3, Folder, Part, RopeConstraint, Vector3, workspace } from '@rally/sdk';
const rig = new Folder();
rig.name = 'WreckingBall';
const hook = new Part(); // anchored: the rope hangs from it
hook.name = 'Hook';
hook.size = new Vector3(2, 2, 2);
hook.position = new Vector3(0, 40, 30);
hook.parent = rig;
const ball = new Part();
ball.name = 'Ball';
ball.shape = 'sphere';
ball.size = new Vector3(6, 6, 6);
ball.position = new Vector3(19, 40, 30); // out to the side
ball.color = Color3.fromHex('#2b2d42');
ball.material = 'metal';
ball.density = 5;
ball.anchored = false;
ball.parent = rig;
const rope = new RopeConstraint();
rope.part0 = hook;
rope.part1 = ball;
rope.pivot0 = new Vector3(0, -1, 0); // the bottom of the hook
rope.pivot1 = new Vector3(0, 3, 0); // the top of the ball
rope.length = 20;
rope.parent = rig;
rig.parent = workspace;

Set enabled to false to let the parts go, and back to true to join them again. Destroying a constraint, or taking it out of the Workspace, also frees the parts.

A constraint takes its starting pose from the parts as they are when it’s set up: a weld keeps the parts’ offset at that moment, and hinge limits count from that angle. Changing any property of a constraint sets it up again from the parts’ current positions.

This trapdoor gives way a moment after someone steps on it, then resets. Build it in Studio as a Model named Trapdoor:

Object Class Set
Frame Part size 1, 1, 6. Anchored.
Door Part size 6, 1, 6, placed so its left edge touches the frame’s right side (3.5 studs to the right of the frame’s center). anchored off.
Hinge HingeConstraint part0 Frame, part1 Door, pivot0 0.5, 0, 0, pivot1 -3, 0, 0, axis 0, 0, 1.
Lock WeldConstraint part0 Frame, part1 Door.
Trap Script The code below.

Leave a hole in the floor under the door. The weld holds the door shut; when the script turns the weld off, only the hinge holds it, and it swings down.

Workspace/Trapdoor/Trap.server.ts
import { BasePart, Character, getChild, Model, script, task, Vector3, WeldConstraint } from '@rally/sdk';
const trap = script.parent;
if (!(trap instanceof Model)) throw new Error('Put this script inside the Trapdoor model');
const door = getChild(trap, 'Door', BasePart);
const lock = getChild(trap, 'Lock', WeldConstraint);
const home = { position: door.position, rotation: door.rotation };
let busy = false;
door.touched.connect((other) => {
if (busy || !(other instanceof Character)) return;
busy = true;
task.delay(0.5, () => {
lock.enabled = false; // only the hinge holds the door now
});
task.delay(4, () => {
// Put the door back, then weld it shut where it is.
door.position = home.position;
door.rotation = home.rotation;
door.velocity = Vector3.zero;
door.angularVelocity = Vector3.zero;
lock.enabled = true;
busy = false;
});
});

Welds and hinges also stop their two parts from colliding with each other, so a door can overlap its frame. Springs and ropes don’t.

Terrain is rolling ground made from a height map. Shape it in Studio with the terrain editor (see Build your world), or from scripts:

Method What it does
getHeight(x, z) The height of the ground at that spot, or null outside the terrain.
sculpt({ x, z, radius, strength, mode }) Raises, lowers, smooths or flattens a circle of ground. mode is 'raise', 'lower', 'smooth' or 'flatten'.
paint({ x, z, radius, material }) Paints grass, sand, rock, snow, mud or ice onto a circle.
generate({ seed, hilliness }) Replaces the ground with random hills. The same seed gives the same hills.

Terrain is solid: characters walk on it, parts land on it, raycasts hit it and parts touching it fire touched with the Terrain. This script plants trees on the ground at random spots:

ServerScriptService/PlantTrees.server.ts
import { Color3, getChild, Part, Terrain, Vector3, workspace } from '@rally/sdk';
const ground = getChild(workspace, 'Terrain', Terrain);
for (let i = 0; i < 40; i++) {
const x = ground.position.x + (Math.random() - 0.5) * ground.size.x;
const z = ground.position.z + (Math.random() - 0.5) * ground.size.z;
const y = ground.getHeight(x, z);
if (y === null) continue;
const trunk = new Part();
trunk.shape = 'cylinder';
trunk.size = new Vector3(1.5, 8, 1.5);
trunk.position = new Vector3(x, y + 4, z); // the bottom sits on the ground
trunk.color = Color3.fromHex('#6d4c41');
trunk.material = 'wood';
trunk.parent = workspace;
}

Every change to terrain sends the whole height map to every player. Sculpting now and then is fine; sculpting every frame isn’t.

workspace.raycast(origin, direction, options) follows a straight line from origin and returns the first thing it hits, or null. The length of direction is how far the ray goes.

Option Default What it does
ignore [] Instances the ray passes through, along with everything inside them.
hitCharacters true Set false to pass through characters.

A hit tells you:

Field What it is
instance What was hit: a BasePart, a Character or the Terrain.
position Where the ray hit, in world space.
normal The direction the hit surface faces.
distance How far from origin the hit is.

Rays pass through parts with canCollide off.

This script checks what each character is standing on, five times a second, and makes them faster on ice:

ServerScriptService/IceSkating.server.ts
import { BasePart, Character, Players, StarterPlayer, task, Vector3, workspace } from '@rally/sdk';
const DOWN = new Vector3(0, -(Character.height / 2 + 1), 0); // a bit past the feet
task.spawn(async () => {
for (;;) {
for (const player of Players.getPlayers()) {
const character = player.character;
if (!character || character.health <= 0) continue;
const hit = workspace.raycast(character.position, DOWN, { ignore: [character] });
const onIce = hit?.instance instanceof BasePart && hit.instance.material === 'ice';
character.walkSpeed = onIce ? 28 : StarterPlayer.characterWalkSpeed;
}
await task.wait(0.2);
}
});

workspace.getPartsInRadius(center, radius) returns every part whose shape overlaps a sphere. It includes parts with canCollide off, but not characters or terrain.

An explosion that knocks nearby parts away and hurts characters close to the blast:

ServerStorage/Explode.ts
import { Players, Vector3, workspace } from '@rally/sdk';
/** Knocks back unanchored parts within `radius`, and damages characters by distance. */
export function explode(center: Vector3, radius: number): void {
for (const part of workspace.getPartsInRadius(center, radius)) {
if (part.anchored) continue;
const away = part.position.sub(center);
const strength = 1 - Math.min(1, away.magnitude / radius); // 1 at the center, 0 at the edge
part.applyImpulse(away.unit.add(new Vector3(0, 0.5, 0)).mul(400 * strength));
}
for (const player of Players.getPlayers()) {
const character = player.character;
if (!character || character.health <= 0) continue;
const distance = character.position.distanceTo(center);
if (distance < radius) character.takeDamage(60 * (1 - distance / radius));
}
}

Import it from any server script with import { explode } from '@game/ServerStorage/Explode';.

Physics on the server and on players’ devices

Section titled “Physics on the server and on players’ devices”
Game server Each player’s device
Unanchored parts Simulated here. Positions are sent to players 20 times a second. Shown where the server says, smoothed between updates. Not simulated.
Constraints Act here. Don’t act.
Touch events Fire here. Never fire.
Your character Checks each move the player sends. Moved here, colliding with the world as this device sees it.
Raycasts and queries Against the server’s world. Against this device’s copy of the world.

A LocalScript can create parts that only that player sees, which is ideal for visual effects. Those parts aren’t simulated: an unanchored part made by a LocalScript stays where you put it, and touch events don’t fire for it. Move effect parts yourself, for example with tweens.

In a 2D game, physics happens on the X/Y plane: parts move along X and Y, turn only around Z, and collide whatever their position.z, which only decides what draws in front. Hinges always turn around Z. See 2D games.

  • Keep unanchored parts to what gameplay needs. Each one is simulated on the server, and every move it makes is sent to every player. Parts that come to rest go to sleep and cost very little until something wakes them.
  • Clean up. Destroy projectiles and debris when you’re done with them, with task.delay(seconds, () => part.destroy()).
  • Don’t rebuild bodies every frame. Changing size, shape, anchored, canCollide, density, friction or restitution rebuilds the part in the simulation.
  • Turn off canTouch on scenery that no script needs to hear about.
  • Animate cosmetic motion on players’ devices. Spinning coins and bobbing pickups don’t need the server. See Server or client.

For the numbers behind these limits, see Limits, and for every physics property, the API reference.