Roblox Moving Platforms and Fall Systems: Building Physics Platformers That Replicate Cleanly

Do you know what happens to a character standing on a platform that the server slides back and forth with TweenService? On the server, nothing looks wrong — the platform moves, the character rides along, and the output window stays quiet.
On the player's screen, however, the platform stutters at the network replication rate, the character slips off the trailing edge, and a fall that should have been survivable sends them back three stages. Our concierge desk has worked through this exact sequence in three recent sessions, so this guide covers the whole chain: carrying players on moving platforms, kill planes and fall damage, and checkpoint respawns that agree between server and client.
Why do players slide off moving platforms in Roblox? The client simulates its own character, but a server-tweened anchored platform arrives only in replicated snapshots with zero velocity. Nothing carries the character along.
Why Players Slide Off Moving Platforms
Every player character in Roblox is network-owned by that player's client, which means the client runs the physics for the character and the server receives the result. That arrangement is what makes movement feel responsive, and it is also why platform logic written purely on the server tends to break.
When the server sets the CFrame of an anchored part every frame, that part has no physical velocity — it simply teleports in small steps. The client receives those steps at the replication rate rather than at 60 frames per second, so the platform appears to jitter, and the Humanoid controller has no floor velocity to inherit.
The underlying problems fall into three categories:
- No inherited velocity. An anchored part moved by CFrame reports an AssemblyLinearVelocity of zero, so the Humanoid treats it as stationary ground. The character stays put in world space while the floor moves out from under it.
- Replication cadence. Server-side CFrame changes reach clients in batches, and interpolation smooths some of that out but not all of it. Fast platforms visibly skip, especially on mobile connections with 150 ms or more of latency.
- Ownership disagreement. The client decides where the character is, while the server decides where the platform is. Any gap between those two answers becomes a slide, a clip, or a missed landing.
All of these issues share a root cause, which is that the character and the platform are being simulated on two different machines. Accordingly, the fixes below either put both on the same machine or make both machines compute the same answer independently.
Three Ways To Move A Platform
There are three practical approaches, and each one trades smoothness against server authority in a different way. Keep in mind that a single game can mix them — slow elevators one way, fast obstacle sweepers another.
| Approach | Client smoothness | Carries players | Server authority | Best for |
|---|---|---|---|---|
| Server TweenService on an anchored part | Poor — steps at replication rate | No, without extra work | Full | Slow doors and decorative motion |
| Unanchored part driven by constraints, server-owned | Good — interpolated physics | Yes, through floor velocity | Full | Elevators, ferries, swinging platforms |
| Anchored part animated on each client from shared server time | Excellent — every frame | Yes, with a manual carry step | Deterministic, not replicated | Fast obby platforms and precise timing |
In practice, the second and third rows cover nearly every platformer. The first row is fine for anything a player never stands on.
Constraint-Driven Platforms
An unanchored platform moved by a PrismaticJoint with a Servo actuator, or by AlignPosition and AlignOrientation, is a real physics object with real velocity. As a result, the Humanoid controller reads that floor velocity and carries the character without any custom code.
The catch is network ownership. By default, Roblox hands ownership of an unanchored assembly to the nearest player, so you need to call SetNetworkOwner(nil) on the platform's root part to pin it to the server — otherwise ownership flips between players as they board, and the platform stutters at every handoff.
Note that a server-owned platform also blocks a common exploit. A client that owns a platform can move it anywhere it likes, and that movement replicates to everyone else; we cover the broader pattern in our guide to Roblox anti-exploit design.
Time-Synced Client Platforms
For fast or precisely timed platforms, the cleanest answer is to stop replicating motion altogether. Each client creates or controls its own copy of the platform and computes its position from workspace:GetServerTimeNow(), which returns a clock that is synchronized across the server and every client.
Because every machine plugs the same timestamp into the same function, every machine draws the platform in the same place. Here is the core of a back-and-forth platform with a six-second cycle:
local RunService = game:GetService("RunService")
local PERIOD = 6
RunService.PreSimulation:Connect(function()
local t = workspace:GetServerTimeNow()
local alpha = (math.sin(t * 2 * math.pi / PERIOD) + 1) / 2
platform.CFrame = startCFrame:Lerp(endCFrame, alpha)
end)
The server can run the same function whenever it needs to validate where a platform was at a given moment. That said, the server should not also set the CFrame of a replicated copy, because its writes would fight the client's writes and reintroduce the stutter.
How do time-synced platforms stay in sync? Every machine computes the platform's position from workspace:GetServerTimeNow() with the same function. No motion is replicated, so clients render smooth frames and still agree on where the platform is.
Carrying The Character On A Client-Animated Platform
A client-animated anchored platform still has zero physical velocity, so the character needs a manual carry. The technique is to measure how far the platform moved this frame and apply the same transform to the character's HumanoidRootPart.
First, raycast a short distance downward from the root part to confirm the character is standing on the platform. Then compute the delta between the platform's previous and current CFrame, and multiply it onto the root part:
local result = workspace:Raycast(root.Position, Vector3.new(0, -4, 0), params)
if result and result.Instance == platform then
local delta = platform.CFrame * lastCFrame:Inverse()
root.CFrame = delta * root.CFrame
end
lastCFrame = platform.CFrame
Run this inside the same PreSimulation callback, capturing lastCFrame just before the platform moves, so the delta always covers exactly one frame. If the platform rotates, the delta rotates the character around the platform's center as well, which is what makes spinning discs ride correctly.
Keep in mind a few refinements that separate a polished carry from a rough one:
- Strip pitch and roll. If a platform tilts, apply only the positional and yaw components of the delta. Otherwise the character tips over with the platform, which the Humanoid will fight every frame.
- Use a 4-stud ray. The default R15 hip height leaves the root part roughly 3 studs above the floor, so a 4-stud ray catches the platform without grabbing ground the character has already left.
- Add the velocity on jump. When the character jumps, add the platform's frame velocity to the root part's AssemblyLinearVelocity. That way a jump off a fast ferry carries forward momentum instead of stopping dead in midair.
Since the character is client-owned, this carry runs only on the owning client and replicates to everyone else as ordinary character movement. For more on why that ownership model behaves the way it does, see our breakdown of Roblox replication and network ownership.
Kill Planes That Cannot Be Missed
Roblox ships with a built-in kill plane: Workspace.FallenPartsDestroyHeight, which defaults to -500 studs. Anything that falls below it is destroyed, and a character that crosses it dies.
For a platformer, -500 is usually far too low, since a player could spend several seconds falling through empty space. In addition, it is a single global value, while most obbies need a different floor for each stage.
The obvious alternative is a large invisible part with a Touched handler, and that approach does work for slow falls. However, Touched on a client-owned character is evaluated from replicated positions, and a character falling at 150 studs per second can pass through a thin part between two updates.
A more reliable pattern is a server-side height check on Heartbeat:
- Per-stage thresholds. Store a KillY attribute on each stage's model, set roughly 30 to 50 studs below its lowest walkable surface. The server looks up the player's current stage and compares the root part's Y position against that value.
- Respawn rather than kill. For obby-style games, teleport the player to their checkpoint instead of setting Health to zero. This skips the character reload entirely and keeps the session moving.
- Client-side early warning. Let the client run the same check and start a screen fade a few studs before the threshold. The server remains the authority, and the client simply makes the transition feel intentional.
All of this keeps the kill plane on the server, where an exploiter cannot disable it by deleting a part from their local workspace.
What is the most reliable kill plane in Roblox? A server Heartbeat check comparing each character's root-part Y position against a per-stage threshold. Thin Touched parts can be skipped by fast falls; a height comparison cannot.
Fall Damage From Velocity, Not Height
Most first attempts at fall damage record the height where the character entered the Freefall state and subtract the height where it landed. This breaks the moment a player jumps upward first, lands on a moving platform, or bounces off a trampoline.
A more robust measure is vertical speed at impact. With Roblox's default gravity of 196.2 studs per second squared, impact speed follows v = √(2gh), which gives these reference points:
- A 7.2-stud drop — the height of a default jump — lands at approximately 53 studs per second.
- A 20-stud drop lands at approximately 88.6 studs per second.
- A 50-stud drop lands at approximately 140 studs per second.
- An 80-stud drop lands at approximately 177 studs per second.
A workable curve sets the safe threshold at 90 studs per second and applies (speed − 90) × 0.75 damage above it. As a result, a 50-stud fall costs about 38 health and an 80-stud fall costs about 65, while anything under 20 studs is free.
How fast does a Roblox character fall after a 50-stud drop? Approximately 140 studs per second at default gravity of 196.2. Basing fall damage on impact speed handles jumps, platforms, and bounces better than height math.
Where that speed comes from matters, too. The client reports AssemblyLinearVelocity for its own character, and an exploiter can simply report zero — so the server should derive vertical speed from the change in replicated position over time instead.
Here is how the server-side tracker fits together:
- Track peak downward speed while airborne. On each Heartbeat, compute vertical speed from the position delta and keep the most negative value seen since the character last stood on ground.
- Detect landing with a raycast. When a short downward ray hits ground and vertical speed drops near zero, apply damage from the recorded peak and reset the tracker.
- Subtract the floor's velocity. If the character lands on a moving platform, use the speed relative to the platform. A player landing on an elevator rising at 20 studs per second hits it harder than the raw number suggests.
- Respect exemptions. Tag water, trampolines, and foam pits with a NoFallDamage attribute and skip damage when the landing ray hits one.
For very large impacts, consider handing the character to a ragdoll state rather than applying damage alone. Our guide to Roblox ragdoll physics covers how to do that without desyncing the character's ownership.
Checkpoints And Respawns That Stay In Sync
A checkpoint system has one job: make sure the server, the client, and the player's saved data all agree on which stage the player has reached. Most checkpoint bugs come from letting the client decide that number.
The reliable pattern is to place a SpawnLocation at each checkpoint, give it a Stage attribute, and handle its Touched event on the server. When a character touches it, the server finds the owning player and applies these checks:
- Sequential order. Accept the touch only if the new stage equals the player's current stage plus one. A player who reaches stage 9 from stage 3 skipped something, either through a shortcut you did not intend or through an exploit.
- Plausible distance. Compare the root part's position to the checkpoint's position. A touch reported from 200 studs away did not happen.
- Single source of truth. Store the stage as an attribute on the Player object. Attributes replicate automatically, so the client UI can read the same number the server uses without a separate RemoteEvent.
Once a checkpoint is accepted, set Player.RespawnLocation to that SpawnLocation. From then on, Roblox spawns the character there whenever it reloads, with no CharacterAdded handler required.
Where should checkpoint logic run in Roblox? On the server. Validate order and distance on Touched, store the stage as a Player attribute, and set Player.RespawnLocation so every respawn lands at the confirmed checkpoint.
Faster Respawns
Players.RespawnTime defaults to 5 seconds, which feels like an eternity in a platformer. Most obbies drop it to between 0.5 and 1 second, and many skip death entirely for falls.
To respawn without a reload, the server calls PivotTo on the character with the checkpoint's CFrame raised by 3 studs, then fires a RemoteEvent telling the owning client to zero its AssemblyLinearVelocity. That second step matters because the client owns the character's physics, and leftover falling speed would otherwise carry over into the new position.
Persisting Progress
Stage progress should survive a disconnect, so write the Stage attribute to a data store when the player leaves and on a periodic autosave. Read it back on join before the first character spawns, then set RespawnLocation so the player lands where they left off.
Remember that data store calls can fail and are rate-limited, so wrap them in retries and never block the spawn on them. We walk through session locking and retry policy in our guide to Roblox data stores.
Testing Under Real Latency
Platform bugs hide in Studio because the client and server run on the same machine with near-zero latency. Before shipping, open Studio's network simulation settings and add incoming replication lag of 150 to 250 ms.
Then walk through a short checklist:
- Ride every platform type. Stand still, walk against the direction of travel, and jump off at the far end. The character should never slide, and jumps should inherit forward momentum.
- Fall from every stage. Confirm the kill plane fires at the expected height and that the respawn lands on the most recent confirmed checkpoint.
- Touch checkpoints out of order. Reach a later checkpoint through a gap or with a teleport command, and confirm the server rejects it.
- Rejoin mid-run. Leave at stage 6, rejoin, and confirm the character spawns at stage 6 rather than the start.
If timing still feels inconsistent between devices, the issue is often frame-rate dependence elsewhere in the codebase. Our piece on fixed-timestep loops explains how to keep simulation logic stable regardless of frame rate, and our Roblox server architecture guide covers where these systems belong as the game grows.
Frequently Asked Questions
The questions below cover the edge cases that come up once the core systems are in place.
What is the default FallenPartsDestroyHeight in Roblox?
It defaults to -500 studs, and any part or character falling below it is destroyed. Most platformers add their own server-side kill threshold 30 to 50 studs below each stage so players respawn quickly.
Do constraint-driven platforms need their network owner set?
Yes — call SetNetworkOwner(nil) on the unanchored platform so the server simulates it. Otherwise ownership passes to the nearest player, the platform stutters at each handoff, and that player can move it.
How short should RespawnTime be for an obby?
The default is 5 seconds, and most obbies set Players.RespawnTime between 0.5 and 1 second. Many skip death for falls entirely and teleport the character to its checkpoint with PivotTo.
Why should fall damage use position deltas instead of reported velocity?
The client owns its character, so an exploiter can report zero velocity to avoid damage. Computing vertical speed from replicated position changes on the server keeps the damage check authoritative.
How do I stop players from skipping checkpoints?
Accept a checkpoint only when its Stage attribute equals the player's current stage plus one, and the character's root part is within a few studs of it. Reject everything else on the server.
Building Your Platformer
Moving platforms, kill planes, fall damage, and checkpoints all depend on one decision made early: which machine owns which piece of the simulation. Get that right, and the rest of the platformer becomes a matter of tuning rather than debugging.
If you are scoping a platformer or a physics-heavy obby and want a second set of eyes on the replication model, our concierge desk works through exactly these systems with Roblox developers. Bring your place file, and we will map your ownership boundaries, name the desync points you are about to hit, and leave you with a build order for the stages ahead.


