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.
Anchored and unanchored parts
Section titled “Anchored and unanchored parts”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:
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.
Moving anchored parts
Section titled “Moving anchored parts”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.
Shapes
Section titled “Shapes”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.
Collisions
Section titled “Collisions”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.
Touch events
Section titled “Touch events”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
canTouchon, including parts withcanCollideoff, fires with theCharacter. UsePlayers.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
Terrainobject.
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:
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 itzone.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); }});Physical properties
Section titled “Physical properties”| 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.
Make things move
Section titled “Make things move”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:
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); }});Moving characters
Section titled “Moving characters”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 assigncharacter.position); - change
walkSpeedandjumpPowerto make it faster or jump higher.
This pad makes characters jump much higher while they stand on it:
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
Section titled “Constraints”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.
Add a constraint in Studio
Section titled “Add a constraint in Studio”-
Select the two parts. The first one you select becomes
part0. -
Choose Insert > Constraints and the kind of constraint. Studio puts it next to the parts and connects it to both.
-
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.
Pivots and axes
Section titled “Pivots and axes”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.
Hinge motors and limits
Section titled “Hinge motors and limits”| 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:
import { Color3, HingeConstraint, Model, Part, Vector3, workspace } from '@rally/sdk';
const mill = new Model();mill.name = 'Windmill';
const tower = new Part(); // anchoredtower.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 faceblades.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 topaxle.pivot1 = new Vector3(0, 0, -0.25); // the back of the blades, at their centeraxle.axis = new Vector3(0, 0, 1); // turn around Zaxle.motorEnabled = true;axle.angularVelocity = 1.5;axle.parent = mill;
mill.parent = workspace;Springs and ropes
Section titled “Springs and ropes”| 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:
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 ithook.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 sideball.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 hookrope.pivot1 = new Vector3(0, 3, 0); // the top of the ballrope.length = 20;rope.parent = rig;
rig.parent = workspace;Turn constraints on and off
Section titled “Turn constraints on and off”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.
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
Section titled “Terrain”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:
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.
Raycasts and spatial queries
Section titled “Raycasts and spatial queries”Cast a ray
Section titled “Cast a ray”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:
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); }});Find parts near a point
Section titled “Find parts near a point”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:
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.
Physics in 2D games
Section titled “Physics in 2D games”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 physics fast
Section titled “Keep physics fast”- 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,frictionorrestitutionrebuilds the part in the simulation. - Turn off
canTouchon 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.