Roblox Vehicle Systems: Building Drivable Cars With Constraints That Replicate Smoothly

Have you ever built a car in Studio that drove perfectly in solo testing, then rubber-banded, jittered, or launched into orbit as soon as a second player joined? If you ship vehicles on Roblox, you almost certainly have.
The cause is rarely the constraints alone, and it is rarely the network alone. It is the seam between them, and this guide covers each side of that seam: wheels built on HingeConstraint and CylindricalConstraint, the network ownership handoff that makes them feel responsive, and the server-side speed checks that keep a client-owned car honest.
Keep in mind that this piece assumes you already know how Roblox replicates physics in general. If not, our Roblox replication guide covers the underlying model, and here we only apply it to one case: a four-wheeled mechanism with a driver in the seat.
A Roblox car that replicates well spins its wheels with HingeConstraint motors and suspends them with CylindricalConstraint or PrismaticConstraint plus a SpringConstraint. The driver gets network ownership when they sit.
How A Constraint-Based Car Is Put Together
Before you touch a single property, it helps to settle the hierarchy. Every wheel in a constraint car is a chain of three physical pieces, and each link in that chain has exactly one job.
From the body outward, the chain looks like this:
- Chassis. This is the heavy root part that the body, seats, and cosmetic meshes weld to. It should carry most of the car's mass so the center of gravity sits low.
- Knuckle. This is a small invisible part that rides on the suspension. On the front wheels it also pivots for steering.
- Wheel. This is a Part with Shape set to Cylinder, attached to the knuckle by a motorized HingeConstraint. It should be the only part that actually touches the road.
Accordingly, each corner has two constraint layers: one between the chassis and the knuckle for travel and steering, and one between the knuckle and the wheel for spin. Keeping those jobs separate means you can tune the suspension without breaking the drive, and vice versa.
Remember that every constraint reads its axis from its attachments. The working axis is the primary (X) axis of Attachment0 and Attachment1, so most of the setup work is orienting attachments correctly.
Which Constraint Should Handle Each Job?
Roblox offers several constraints that could plausibly drive a wheel, and the differences start to matter once replication enters the picture. Here is how the common choices compare for vehicle work:
| Constraint | Freedom | Best job in a car | Watch out for |
|---|---|---|---|
| HingeConstraint (Motor) | Rotation around one axis | Wheel spin between knuckle and wheel | Mirrored wheels need opposite AngularVelocity signs |
| HingeConstraint (Servo) | Rotation to a target angle | Steering pivot on cars with no suspension | No travel, so every bump goes straight into the chassis |
| CylindricalConstraint | Slide along and rotate around one axis | Front suspension travel plus steering in one constraint | The slider has no stiffness of its own and needs a SpringConstraint |
| PrismaticConstraint | Slide along one axis | Rear suspension travel with no steering | Without limits the knuckle can drop out of the wheel well |
| SpringConstraint | Distance with stiffness and damping | Holds ride height and absorbs bumps | Under-damped springs make the car bounce after every ramp |
In short, the HingeConstraint spins the tire, the CylindricalConstraint or PrismaticConstraint decides where the tire can move, and the SpringConstraint decides how hard it pushes back. That division of labor is the whole architecture.
Use a HingeConstraint with ActuatorType set to Motor for wheel spin. Use a CylindricalConstraint on each front knuckle, where one vertical axis handles both suspension travel and servo steering.
Setting Up HingeConstraint Wheels
The wheel hinge is the simplest link, and it is also where most first builds go wrong. The problem is almost always how the attachments are oriented, not a bad property value.
Here is a setup that holds up in production:
- Attachment placement. Put Attachment0 in the knuckle and Attachment1 in the wheel, both at the wheel's center. Rotate both so their X axis points along the axle, outward from the car.
- Actuator type. Set ActuatorType to Motor on driven wheels and to None on free-rolling ones. Leave AngularVelocity at 0 in Studio, because the drive script sets it at runtime.
- Torque. Set MotorMaxTorque high enough to push the car's full mass up your steepest slope. On a mid-weight car, values in the tens of thousands are a common starting range; lower it until the wheels start to spin out, then raise it slightly.
- Acceleration. Set MotorMaxAcceleration to a finite value instead of the default infinity. Somewhere around 20 to 60 radians per second squared makes the throttle ramp up instead of acting like an on/off switch.
Because both sides' attachments point outward, the left and right wheels spin in opposite directions for the same AngularVelocity. That is why every drive script negates one side — it comes from the geometry and is not a bug.
What's more, a motor held at 0 AngularVelocity with a high MotorMaxTorque acts as a brake. Keep that in mind, because it becomes useful when ownership returns to the server later.
Set collision groups first. Before you tune anything, put the wheels in a collision group that does not collide with the chassis group. A wheel grinding against its own fender looks like weak torque and can cost you an afternoon of pointless tuning.
Using CylindricalConstraint For Suspension And Steering
The CylindricalConstraint is the least intuitive piece and also the most useful. It lets a part slide along an axis and rotate around that same axis, and pointed vertically, that is exactly what a front knuckle does.
Sliding along the vertical axis is suspension travel, and rotating around it is steering. As a result, one constraint replaces a prismatic-plus-hinge stack that would otherwise need an extra invisible part.
The setup for each front corner includes:
- Axis orientation. Place Attachment0 on the chassis and Attachment1 on the knuckle, and rotate both so X points straight down. If the two attachments disagree on direction, the constraint fights itself.
- Linear limits. Enable LimitsEnabled and set LowerLimit and UpperLimit to your travel range, roughly -1 to 0.5 studs for a street car. Without limits, the knuckle falls through the floor the first time the car goes airborne.
- Steering servo. Set AngularActuatorType to Servo, give it a high ServoMaxTorque, and set AngularSpeed to around 4 to 6 radians per second. The drive script writes TargetAngle, usually clamped to 30 degrees either way.
- Spring. Add a SpringConstraint between the same chassis and knuckle points. Give it a FreeLength slightly longer than the static ride height so it is preloaded at rest.
For the rear corners, swap the CylindricalConstraint for a PrismaticConstraint with the same limits. The rear knuckles should never rotate, and a PrismaticConstraint makes rotation impossible rather than merely unlikely.
If you want Ackermann steering, where the inner wheel turns more sharply than the outer one, compute a separate TargetAngle for each side. For most arcade handling, though, one shared angle is fine and far easier to tune.
Start SpringConstraint Stiffness at chassis mass times gravity, divided by four wheels, divided by the compression you want in studs. Set Damping near 10% of Stiffness, then tune on rough terrain.
Tuning Mass, Friction, And Wheel Shape
Once the constraints move correctly, the car still has to feel right, and feel comes mostly from mass distribution. A car that flips on every corner almost always has too much mass above the axles.
The settings that matter most include:
- Massless cosmetics. Set Massless on every decorative MeshPart welded to the chassis. Otherwise a detailed body kit quietly raises the center of gravity.
- Heavy, low chassis. Use CustomPhysicalProperties to make the chassis floor dense, so the mass sits low. Keep the knuckles light so the suspension responds quickly.
- Wheel friction. Give the wheels a Friction of around 1 and a FrictionWeight above 1 so the tire, not the road surface, controls grip. Lower both for drift-style handling.
- True cylinders. Use a Part with Shape set to Cylinder as the physical wheel, and weld a non-colliding, Massless MeshPart tire over it for looks. Mesh collision shapes are made of flat faces, and a faceted wheel bumps at speed.
All of these settings replicate with the model, so they behave the same no matter who owns the car. The next section is where that stops being true.
Why Network Ownership Decides How The Car Feels
At any given moment, Roblox simulates each assembly on exactly one machine, called the network owner. Every other machine sees replicated positions and interpolates between them.
If the server owns the car, the driver's input travels to the server, the server simulates it, and the result travels back. That round trip adds a full ping of delay to every throttle and steering change, and at 120 milliseconds it feels like steering a boat.
By contrast, when the driver's client owns the car, input and simulation happen on the same machine with no network delay. The server and other players then receive the resulting positions, which is exactly the trade you want for a vehicle.
That said, client ownership makes the client the authority on where the car is. This is why the ownership handoff and server-side verification have to be designed together, and our Roblox anti-exploit guide covers the broader trust model behind that pairing.
Give the driving player network ownership so their inputs simulate locally with no round trip. Return ownership to the server on exit so the last driver cannot fling the parked car.
Handing Ownership To The Driver And Back
Automatic ownership assignment will often give the car to a nearby player, but "often" is not a design. Instead, set ownership explicitly from a server Script whenever the seat's Occupant changes:
local Players = game:GetService("Players")
local car = script.Parent
local seat = car.DriverSeat
local graceUntil = 0
local function setOwner(player)
for _, part in car:GetDescendants() do
if part:IsA("BasePart") and part:CanSetNetworkOwnership() then
part:SetNetworkOwner(player)
end
end
end
seat:GetPropertyChangedSignal("Occupant"):Connect(function()
local humanoid = seat.Occupant
graceUntil = os.clock() + 1.5
if humanoid then
setOwner(Players:GetPlayerFromCharacter(humanoid.Parent))
else
setOwner(nil)
end
end)
Several details in that script matter:
- Every part. The loop covers every part the car can hand over, so no wheel or knuckle ends up with a different owner than the chassis. A wheel simulated on the server under a body simulated on the client is a classic source of wobble.
- The CanSetNetworkOwnership check. It skips anchored parts and anything welded to them, where SetNetworkOwner would throw an error.
- Exiting to nil. Passing nil returns the simulation to the server. The server never saw the client's local motor writes, so its hinges still hold AngularVelocity at 0 with full torque, and the car brakes itself to a stop.
Be aware that the handoff is not instantaneous. For about a second after the change, positions can snap as the new owner picks up from the last replicated state, so the script records a grace window that the speed check below respects.
In addition, if the driver dies or is thrown out in a crash, Occupant changes to nil and ownership comes back cleanly. If you ragdoll the ejected driver, destroy the SeatWeld before enabling the ragdoll so the character and the car split into separate assemblies first — our Roblox ragdoll physics guide covers the ragdoll side of that sequence.
Driving The Motors From The Client
With the driver owning the car, the drive loop belongs in a LocalScript in StarterPlayerScripts that binds when the local Humanoid sits in a VehicleSeat. Constraint changes made on the client do not replicate, and they do not need to: the client is running the simulation, and the resulting motion does replicate.
local RunService = game:GetService("RunService")
local MAX_SPEED = 60 -- studs per second
local WHEEL_RADIUS = 1.5
local MAX_STEER = 30 -- degrees
local function bindCar(car)
local seat = car.DriverSeat
local rearLeft = car.RearLeft.Hinge
local rearRight = car.RearRight.Hinge
local frontLeft = car.FrontLeft.Cylindrical
local frontRight = car.FrontRight.Cylindrical
return RunService.PreSimulation:Connect(function()
local spin = seat.ThrottleFloat * MAX_SPEED / WHEEL_RADIUS
rearLeft.AngularVelocity = spin
rearRight.AngularVelocity = -spin
local steer = seat.SteerFloat * MAX_STEER
frontLeft.TargetAngle = steer
frontRight.TargetAngle = steer
end)
end
Here is what the loop does:
- ThrottleFloat and SteerFloat. VehicleSeat fills these from the occupant's keyboard, gamepad, or touch controls, in a range from -1 to 1. If you use custom bindings, our Roblox input actions guide shows how to feed the same values from your own action map.
- Speed to angular velocity. Dividing the target linear speed by the wheel radius gives radians per second. A MAX_SPEED of 60 studs per second on a 1.5-stud wheel works out to 40 radians per second.
- PreSimulation timing. Motor targets are physics inputs, so writing them in PreSimulation applies them on the very next physics step. Disconnect the returned connection when the Humanoid's Seated event reports that the player got out.
Note that this script writes motor targets and nothing else. It never sets CFrame or velocity directly, which keeps the car inside the physics solver, where the server's checks can reason about it.
Once the drive loop feels right, pair it with a chase camera that lags slightly behind the chassis. Our Roblox camera systems guide covers the spring-follow math that keeps the camera from amplifying suspension bounce.
Server-Side Speed Checks That Do Not Punish Honest Drivers
Because the client owns the car, an exploiter can set any AngularVelocity or MotorMaxTorque they like, or skip the motors entirely and move the chassis directly. The server cannot block those writes, but it can measure what they produce.
The reliable signal is how far the chassis moves horizontally over a sampling window, measured on the server from its replicated position. The owner can fake velocity just as easily as position, so distance over time is the one measure that reflects what other players actually see.
local RunService = game:GetService("RunService")
local chassis = car.Chassis
local MAX_SPEED = 60
local TOLERANCE = 1.35
local WINDOW = 0.5
local elapsed = 0
local strikes = 0
local lastPos = chassis.Position
local lastGood = car:GetPivot()
RunService.Heartbeat:Connect(function(dt)
elapsed += dt
if elapsed > WINDOW then
local pos = chassis.Position
local flat = Vector3.new(pos.X - lastPos.X, 0, pos.Z - lastPos.Z).Magnitude / elapsed
lastPos = pos
elapsed = 0
if os.clock() > graceUntil and flat > MAX_SPEED * TOLERANCE then
strikes += 1
if strikes > 2 then
setOwner(nil)
car:PivotTo(lastGood)
lastPos = lastGood.Position
graceUntil = os.clock() + 1.5
strikes = 0
end
else
strikes = math.max(strikes - 1, 0)
if strikes == 0 then
lastGood = car:GetPivot()
end
end
end
end)
This loop lives in the same server Script as the ownership handler, so it shares setOwner and graceUntil. It also assumes Chassis is the model's PrimaryPart, so the pivot and the chassis position match.
Each constant in the check exists to prevent a false positive:
- Horizontal only. Falling off a cliff produces vertical speeds far above any driving speed. Dropping the Y component keeps gravity out of the verdict.
- Half-second window. Replicated positions arrive in bursts, so single-frame deltas spike even for honest clients. Averaging over 0.5 seconds smooths out that jitter.
- 35% tolerance. Downhill runs and ramp landings can legitimately exceed motor speed. A 1.35 multiplier absorbs those while still catching the 2x and 10x speeds that speed exploits produce.
- Strikes, not instant verdicts. The server waits for three violating windows in a row (1.5 seconds of impossible speed) before acting. Each clean window clears one strike.
- Ownership grace. The 1.5-second window after any Occupant change covers the handoff snap described earlier.
When the check trips, the response is proportionate: the server takes ownership back, moves the car to its last verified position, and logs the event. If the driver is still seated after a short cooldown, give ownership back, and route repeat offenders to the escalation path in our Roblox moderation systems guide instead of kicking on the first offense.
Measure the chassis's horizontal distance over half-second windows on the server. Allow roughly 35% above max speed, and act only after three violations in a row.
Testing The Loop Before Players Do
Solo Play in Studio hides every problem in this guide, because the server and client run on the same machine. Instead, start a local server with two or more players from the Test tab so ownership actually moves between machines.
A useful test pass includes:
- Visualizing ownership. Turn on Are Owners Shown in Studio's Physics settings to outline parts by owner. Every part of the car should change color together when the driver sits.
- Adding latency. Set Incoming Replication Lag to about 0.2 seconds and confirm the speed check never trips during normal driving, including ramps and downhill runs.
- Scripting the attack. From the driver's client, set AngularVelocity to five times its normal value. The car should reset within about two seconds.
- Exiting at speed. Jump out at full throttle and confirm the car brakes under server ownership instead of rolling away.
Test vehicles on a Studio local server with two or more players and added replication lag. Solo Play runs the server and client on one machine and hides ownership bugs.
Additionally, move the violation check into a ModuleScript so automated tests can cover the tolerance math. Our Roblox unit testing guide walks through that setup.
Once you reach dozens of cars per server, measure the per-car Heartbeat loops before assuming they are cheap. The Roblox performance profiling guide shows how to tell whether they should be merged into a single loop that iterates over all cars.
Where To Go Next
A drivable car is a small mechanism with a lot of surface area: physics tuning, input, ownership, and trust all meet at the seat. Get the constraint chain right first, then the handoff, then the server check, and each layer stays simple.
If you are building vehicles into a larger game, our Roblox server architecture guide covers where vehicle spawning and cleanup belong. Our Luau scripting patterns guide helps keep the per-car scripts from multiplying as your fleet grows.


