Tutorial · Gameplay

How to make a drivable car in Roblox Studio

Most cars that come out of the Toolbox do not drive: their scripts were stripped, or they use old body movers that no longer behave. A car that works today is a few parts held together by constraints: motors spin the wheels, servos turn the front ones, and a VehicleSeat reads the driver's keys. This guide builds one in code, with a pad that spawns a car for each player, so you can see every piece before you put your own body on it.

Updated 25 October 20266 min readIntermediateLuau

In short

Weld a VehicleSeat to a chassis part. Join each wheel to the chassis with a HingeConstraint set to Motor, with its axis along the axle. For steering, put each front wheel on a small knuckle part joined to the chassis by a second hinge set to Servo, with an upright axis. A server script reads the seat's ThrottleFloat and SteerFloat and sets the motors' AngularVelocity (speed divided by wheel radius) and the servos' TargetAngle.

  • Give the driver network ownership of the car with SetNetworkOwner, or it lags behind their keys.
  • Spin the wheels with constraints, not by moving the car with CFrame: physics keeps it on the ground and lets it hit things.
  • One car per player: destroy the old one before spawning the next, and when the player leaves.

Get the script

Car script with a spawner pad

One server Script builds a drivable car with motor and servo hinges, gives the driver network ownership and spawns one car per player from a pad.

Script in ServerScriptService named CarSpawner
-- Script in ServerScriptService named CarSpawner.
-- Builds a drivable car from parts and constraints when a player uses the
-- "Spawn car" prompt on Workspace.CarPad. One car per player.
local Players = game:GetService("Players")

local TOP_SPEED = 60 -- studs per second
local REVERSE_SPEED = 25
local DRIVE_TORQUE = 6000 -- how hard the wheels push
local BRAKE_TORQUE = 600 -- slows the car down when you let go
local STEER_ANGLE = 30 -- degrees the front wheels turn
local WHEEL_RADIUS = 1.4

local pad = workspace:WaitForChild("CarPad") :: BasePart
local cars: { [Player]: Model } = {}

local function newPart(name: string, size: Vector3, cframe: CFrame, parent: Instance): Part
	local p = Instance.new("Part")
	p.Name = name
	p.Size = size
	p.CFrame = cframe
	p.TopSurface = Enum.SurfaceType.Smooth
	p.BottomSurface = Enum.SurfaceType.Smooth
	p.Parent = parent
	return p
end

local function weld(a: BasePart, b: BasePart)
	local w = Instance.new("WeldConstraint")
	w.Part0 = a
	w.Part1 = b
	w.Parent = a
end

-- An attachment whose X axis (the hinge axis) points along worldCFrame's X axis
local function attach(part: BasePart, name: string, worldCFrame: CFrame): Attachment
	local a = Instance.new("Attachment")
	a.Name = name
	a.CFrame = part.CFrame:ToObjectSpace(worldCFrame)
	a.Parent = part
	return a
end

type Wheel = { drive: HingeConstraint, steer: HingeConstraint? }

local function buildCar(at: CFrame): (Model, VehicleSeat, BasePart, { Wheel })
	local car = Instance.new("Model")
	car.Name = "Car"
	local R, U, F = at.RightVector, at.UpVector, at.LookVector

	-- The chassis: everything else hangs off this part
	local base = newPart("Base", Vector3.new(6, 1, 10), at * CFrame.new(0, WHEEL_RADIUS + 0.5, 0), car)
	base.Color = Color3.fromRGB(200, 40, 50)
	car.PrimaryPart = base

	local body = newPart("Body", Vector3.new(5.6, 2, 5), at * CFrame.new(0, WHEEL_RADIUS + 2, 1), car)
	body.Color = Color3.fromRGB(230, 60, 70)
	body.Massless = true
	weld(base, body)

	local seat = Instance.new("VehicleSeat")
	seat.Name = "DriveSeat"
	seat.Size = Vector3.new(2, 1, 2)
	seat.CFrame = at * CFrame.new(0, WHEEL_RADIUS + 1.5, -2)
	seat.HeadsUpDisplay = false
	seat.Massless = true
	seat.Parent = car
	weld(base, seat)

	local wheels: { Wheel } = {}
	for _, corner in { Vector3.new(-3.6, 0, -3.5), Vector3.new(3.6, 0, -3.5), Vector3.new(-3.6, 0, 3.5), Vector3.new(3.6, 0, 3.5) } do
		local front = corner.Z < 0 -- the car faces -Z, the pad's look direction
		local pos = at:PointToWorldSpace(Vector3.new(corner.X, WHEEL_RADIUS, corner.Z))

		local wheel = newPart("Wheel", Vector3.new(1, WHEEL_RADIUS * 2, WHEEL_RADIUS * 2), CFrame.fromMatrix(pos, R, U), car)
		wheel.Shape = Enum.PartType.Cylinder -- a cylinder's round faces are on its X axis
		wheel.Color = Color3.fromRGB(30, 30, 34)
		wheel.CustomPhysicalProperties = PhysicalProperties.new(0.7, 1.5, 0, 100, 1)

		local hub: BasePart = base
		local steer: HingeConstraint? = nil
		if front then
			-- Front wheels turn on a small knuckle part, steered by a servo hinge with an up axis
			local knuckle = newPart("Knuckle", Vector3.new(0.5, 0.5, 0.5), CFrame.fromMatrix(pos, R, U), car)
			knuckle.CanCollide = false
			knuckle.Transparency = 1
			knuckle.Massless = true
			local axisUp = CFrame.fromMatrix(pos, U, F)
			local servo = Instance.new("HingeConstraint")
			servo.Attachment0 = attach(base, "SteerBase", axisUp)
			servo.Attachment1 = attach(knuckle, "SteerKnuckle", axisUp)
			servo.ActuatorType = Enum.ActuatorType.Servo
			servo.ServoMaxTorque = 20000
			servo.AngularSpeed = 5
			servo.LimitsEnabled = true
			servo.LowerAngle = -STEER_ANGLE
			servo.UpperAngle = STEER_ANGLE
			servo.Parent = knuckle
			hub = knuckle
			steer = servo
		end

		-- Every wheel spins on a motor hinge whose axis points to the car's left:
		-- a positive AngularVelocity about that axis rolls the wheel forward
		local axisLeft = CFrame.fromMatrix(pos, -R, U)
		local drive = Instance.new("HingeConstraint")
		drive.Attachment0 = attach(hub, "DriveHub", axisLeft)
		drive.Attachment1 = attach(wheel, "DriveWheel", axisLeft)
		drive.ActuatorType = Enum.ActuatorType.Motor
		drive.MotorMaxTorque = BRAKE_TORQUE
		drive.AngularVelocity = 0
		drive.Parent = wheel

		table.insert(wheels, { drive = drive, steer = steer })
	end
	return car, seat, base, wheels
end

-- Reads the driver's keys (W/S or the thumbstick) from the seat and turns them into wheel motion
local function wireControls(seat: VehicleSeat, base: BasePart, wheels: { Wheel })
	local function update()
		local throttle = seat.ThrottleFloat
		local speed = if throttle > 0 then TOP_SPEED * throttle else REVERSE_SPEED * throttle
		for _, w in wheels do
			w.drive.AngularVelocity = speed / WHEEL_RADIUS
			w.drive.MotorMaxTorque = if throttle == 0 then BRAKE_TORQUE else DRIVE_TORQUE
			if w.steer then
				-- SteerFloat is 1 for right (D); a positive servo angle about the up axis turns left
				w.steer.TargetAngle = -seat.SteerFloat * STEER_ANGLE
			end
		end
	end
	seat:GetPropertyChangedSignal("ThrottleFloat"):Connect(update)
	seat:GetPropertyChangedSignal("SteerFloat"):Connect(update)

	-- The driver's computer simulates the car, so it reacts without lag
	seat:GetPropertyChangedSignal("Occupant"):Connect(function()
		local humanoid = seat.Occupant
		local character = humanoid and humanoid.Parent
		local driver = if character and character:IsA("Model") then Players:GetPlayerFromCharacter(character) else nil
		if driver then
			base:SetNetworkOwner(driver)
		elseif base:IsDescendantOf(workspace) then
			base:SetNetworkOwnershipAuto()
			update()
		end
	end)
end

local prompt = Instance.new("ProximityPrompt")
prompt.ActionText = "Spawn car"
prompt.ObjectText = "Car"
prompt.RequiresLineOfSight = false
prompt.Parent = pad

prompt.Triggered:Connect(function(player: Player)
	local old = cars[player]
	if old then
		old:Destroy()
	end
	-- the car appears in front of the pad, facing the same way as the pad
	local spawnAt = pad.CFrame * CFrame.new(0, pad.Size.Y / 2, -12)
	local car, seat, base, wheels = buildCar(spawnAt)
	car.Parent = workspace
	cars[player] = car
	wireControls(seat, base, wheels)

	local character = player.Character
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	if humanoid then
		seat:Sit(humanoid)
	end
end)

Players.PlayerRemoving:Connect(function(player: Player)
	local car = cars[player]
	if car then
		car:Destroy()
		cars[player] = nil
	end
end)

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: a pad and one Script
Workspace
  CarPad Part new
ServerScriptService
  CarSpawner Script new

Settings you can change

TOP_SPEEDIn the script
Top speed in studs per second. A character walks at 16. Default: 60.
STEER_ANGLEIn the script
How far the front wheels turn, in degrees. Lower is steadier at speed. Default: 30.
DRIVE_TORQUEIn the script
How hard the wheels push. Raise it for steep hills or a heavier body. Default: 6000.

The pad needs empty, flat ground about 12 studs in front of it, the way its front face points: the car appears there, facing the same way.

Type-checked with luau-lsp strict against Roblox's API definitions on 25 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Add a drivable car to my game: a pad in Workspace with a Spawn car prompt, one car per player, built from a VehicleSeat, motor HingeConstraints on four wheels and servo steering on the front wheels, with the driver given network ownership so it drives smoothly.

On this page
  1. Get the script
  2. How it works
  3. 1. The pad
  4. 2. The script
  5. 3. Driving
  6. 4. Your own car
  7. Common errors
  8. With RoCode
  9. Questions
  10. Sources

How a constraint car works

Every part of the car is unanchored and simulated by physics. Only the constraints hold it together.

The parts of the car
PartJoined byJob
BaseNothing: everything joins to itThe chassis. It carries the mass and is the part the driver owns
Body, DriveSeatWeldConstraint to BaseThe look, and the seat that reads W, A, S, D or a thumbstick
Rear wheelsHingeConstraint (Motor) to BaseSpin at the speed the script asks for
KnucklesHingeConstraint (Servo) to BaseTurn left and right with an upright axis
Front wheelsHingeConstraint (Motor) to a knuckleSpin like the rear wheels and turn with their knuckle

A HingeConstraint turns around the X axis of its attachments. The script builds each attachment with CFrame.fromMatrix so its X axis points where it should: to the car's left for the wheel motors, and straight up for the steering servos.

Step 1: Make the spawn pad

  1. Insert a Part in Workspace, name it CarPad, make it about 8 by 1 by 8 studs and set Anchored on.
  2. Turn it to face an open road: the car spawns 12 studs in front of it. If the car appears behind the pad instead, turn the pad around 180 degrees.

Step 2: Add the car script

  1. Insert a Script in ServerScriptService named CarSpawner and paste the code below.
  2. Press Play, walk to the pad and hold E on Spawn car. You are seated in a new car: W and S drive, A and D steer, Space gets out.
Script in ServerScriptService named CarSpawner
-- Script in ServerScriptService named CarSpawner.
-- Builds a drivable car from parts and constraints when a player uses the
-- "Spawn car" prompt on Workspace.CarPad. One car per player.
local Players = game:GetService("Players")

local TOP_SPEED = 60 -- studs per second
local REVERSE_SPEED = 25
local DRIVE_TORQUE = 6000 -- how hard the wheels push
local BRAKE_TORQUE = 600 -- slows the car down when you let go
local STEER_ANGLE = 30 -- degrees the front wheels turn
local WHEEL_RADIUS = 1.4

local pad = workspace:WaitForChild("CarPad") :: BasePart
local cars: { [Player]: Model } = {}

local function newPart(name: string, size: Vector3, cframe: CFrame, parent: Instance): Part
	local p = Instance.new("Part")
	p.Name = name
	p.Size = size
	p.CFrame = cframe
	p.TopSurface = Enum.SurfaceType.Smooth
	p.BottomSurface = Enum.SurfaceType.Smooth
	p.Parent = parent
	return p
end

local function weld(a: BasePart, b: BasePart)
	local w = Instance.new("WeldConstraint")
	w.Part0 = a
	w.Part1 = b
	w.Parent = a
end

-- An attachment whose X axis (the hinge axis) points along worldCFrame's X axis
local function attach(part: BasePart, name: string, worldCFrame: CFrame): Attachment
	local a = Instance.new("Attachment")
	a.Name = name
	a.CFrame = part.CFrame:ToObjectSpace(worldCFrame)
	a.Parent = part
	return a
end

type Wheel = { drive: HingeConstraint, steer: HingeConstraint? }

local function buildCar(at: CFrame): (Model, VehicleSeat, BasePart, { Wheel })
	local car = Instance.new("Model")
	car.Name = "Car"
	local R, U, F = at.RightVector, at.UpVector, at.LookVector

	-- The chassis: everything else hangs off this part
	local base = newPart("Base", Vector3.new(6, 1, 10), at * CFrame.new(0, WHEEL_RADIUS + 0.5, 0), car)
	base.Color = Color3.fromRGB(200, 40, 50)
	car.PrimaryPart = base

	local body = newPart("Body", Vector3.new(5.6, 2, 5), at * CFrame.new(0, WHEEL_RADIUS + 2, 1), car)
	body.Color = Color3.fromRGB(230, 60, 70)
	body.Massless = true
	weld(base, body)

	local seat = Instance.new("VehicleSeat")
	seat.Name = "DriveSeat"
	seat.Size = Vector3.new(2, 1, 2)
	seat.CFrame = at * CFrame.new(0, WHEEL_RADIUS + 1.5, -2)
	seat.HeadsUpDisplay = false
	seat.Massless = true
	seat.Parent = car
	weld(base, seat)

	local wheels: { Wheel } = {}
	for _, corner in { Vector3.new(-3.6, 0, -3.5), Vector3.new(3.6, 0, -3.5), Vector3.new(-3.6, 0, 3.5), Vector3.new(3.6, 0, 3.5) } do
		local front = corner.Z < 0 -- the car faces -Z, the pad's look direction
		local pos = at:PointToWorldSpace(Vector3.new(corner.X, WHEEL_RADIUS, corner.Z))

		local wheel = newPart("Wheel", Vector3.new(1, WHEEL_RADIUS * 2, WHEEL_RADIUS * 2), CFrame.fromMatrix(pos, R, U), car)
		wheel.Shape = Enum.PartType.Cylinder -- a cylinder's round faces are on its X axis
		wheel.Color = Color3.fromRGB(30, 30, 34)
		wheel.CustomPhysicalProperties = PhysicalProperties.new(0.7, 1.5, 0, 100, 1)

		local hub: BasePart = base
		local steer: HingeConstraint? = nil
		if front then
			-- Front wheels turn on a small knuckle part, steered by a servo hinge with an up axis
			local knuckle = newPart("Knuckle", Vector3.new(0.5, 0.5, 0.5), CFrame.fromMatrix(pos, R, U), car)
			knuckle.CanCollide = false
			knuckle.Transparency = 1
			knuckle.Massless = true
			local axisUp = CFrame.fromMatrix(pos, U, F)
			local servo = Instance.new("HingeConstraint")
			servo.Attachment0 = attach(base, "SteerBase", axisUp)
			servo.Attachment1 = attach(knuckle, "SteerKnuckle", axisUp)
			servo.ActuatorType = Enum.ActuatorType.Servo
			servo.ServoMaxTorque = 20000
			servo.AngularSpeed = 5
			servo.LimitsEnabled = true
			servo.LowerAngle = -STEER_ANGLE
			servo.UpperAngle = STEER_ANGLE
			servo.Parent = knuckle
			hub = knuckle
			steer = servo
		end

		-- Every wheel spins on a motor hinge whose axis points to the car's left:
		-- a positive AngularVelocity about that axis rolls the wheel forward
		local axisLeft = CFrame.fromMatrix(pos, -R, U)
		local drive = Instance.new("HingeConstraint")
		drive.Attachment0 = attach(hub, "DriveHub", axisLeft)
		drive.Attachment1 = attach(wheel, "DriveWheel", axisLeft)
		drive.ActuatorType = Enum.ActuatorType.Motor
		drive.MotorMaxTorque = BRAKE_TORQUE
		drive.AngularVelocity = 0
		drive.Parent = wheel

		table.insert(wheels, { drive = drive, steer = steer })
	end
	return car, seat, base, wheels
end

-- Reads the driver's keys (W/S or the thumbstick) from the seat and turns them into wheel motion
local function wireControls(seat: VehicleSeat, base: BasePart, wheels: { Wheel })
	local function update()
		local throttle = seat.ThrottleFloat
		local speed = if throttle > 0 then TOP_SPEED * throttle else REVERSE_SPEED * throttle
		for _, w in wheels do
			w.drive.AngularVelocity = speed / WHEEL_RADIUS
			w.drive.MotorMaxTorque = if throttle == 0 then BRAKE_TORQUE else DRIVE_TORQUE
			if w.steer then
				-- SteerFloat is 1 for right (D); a positive servo angle about the up axis turns left
				w.steer.TargetAngle = -seat.SteerFloat * STEER_ANGLE
			end
		end
	end
	seat:GetPropertyChangedSignal("ThrottleFloat"):Connect(update)
	seat:GetPropertyChangedSignal("SteerFloat"):Connect(update)

	-- The driver's computer simulates the car, so it reacts without lag
	seat:GetPropertyChangedSignal("Occupant"):Connect(function()
		local humanoid = seat.Occupant
		local character = humanoid and humanoid.Parent
		local driver = if character and character:IsA("Model") then Players:GetPlayerFromCharacter(character) else nil
		if driver then
			base:SetNetworkOwner(driver)
		elseif base:IsDescendantOf(workspace) then
			base:SetNetworkOwnershipAuto()
			update()
		end
	end)
end

local prompt = Instance.new("ProximityPrompt")
prompt.ActionText = "Spawn car"
prompt.ObjectText = "Car"
prompt.RequiresLineOfSight = false
prompt.Parent = pad

prompt.Triggered:Connect(function(player: Player)
	local old = cars[player]
	if old then
		old:Destroy()
	end
	-- the car appears in front of the pad, facing the same way as the pad
	local spawnAt = pad.CFrame * CFrame.new(0, pad.Size.Y / 2, -12)
	local car, seat, base, wheels = buildCar(spawnAt)
	car.Parent = workspace
	cars[player] = car
	wireControls(seat, base, wheels)

	local character = player.Character
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	if humanoid then
		seat:Sit(humanoid)
	end
end)

Players.PlayerRemoving:Connect(function(player: Player)
	local car = cars[player]
	if car then
		car:Destroy()
		cars[player] = nil
	end
end)

Step 3: How driving works

A VehicleSeat sets ThrottleFloat from -1 to 1 for W and S and SteerFloat from -1 to 1 for A and D (and the thumbstick on a phone or controller). wireControls listens for both and turns them into motion:

  • Speed: a wheel rolling at v studs per second spins at v / radius radians per second, so each motor gets speed / WHEEL_RADIUS. Reverse is capped at REVERSE_SPEED.
  • Braking: with no throttle the motors aim for 0 spin with BRAKE_TORQUE, a gentle brake, so the car rolls to a stop instead of drifting forever.
  • Steering: each front servo gets TargetAngle = -SteerFloat * STEER_ANGLE. Positive angles about an upright axis turn left, and D makes SteerFloat positive, hence the minus.

When someone sits, the seat's Occupant changes and the script calls base:SetNetworkOwner(driver). Physics for the whole car then runs on the driver's device, so it answers their keys at once. When they get out, ownership goes back to automatic.

Step 4: Tune it and use your own body

Tuning
If the car...Change
Is too slow up hillsRaise DRIVE_TORQUE
Flips in cornersLower STEER_ANGLE or TOP_SPEED, or widen the wheel corners in buildCar
Slides in cornersRaise the wheels' friction, the second number in CustomPhysicalProperties
Stops too sharplyLower BRAKE_TORQUE

To use your own car model, keep the chassis, wheels and constraints from buildCar and replace the Body part. Weld every part of your body to Base with a WeldConstraint, set them unanchored and Massless, and turn off CanCollide on parts that hang low, so they do not scrape the ground.

Common errors and fixes

If the car does not move the way it should, check these first.

The car drives backwards or steers the wrong way

An attachment axis points the other way in your build. Put a minus in front of speed / WHEEL_RADIUS for driving, or remove the minus from the TargetAngle line for steering.

Nothing happens when I press W

Check you are in the DriveSeat (a VehicleSeat, not a Seat) and that no part of the car is Anchored. An anchored part freezes the whole car.

The car jitters or lags behind my keys

The driver needs network ownership. Check the Occupant handler runs, and that the car has no anchored parts, which make SetNetworkOwner fail.

Infinite yield possible on Workspace:WaitForChild("CarPad")

The pad is missing or named differently. It must be a part named CarPad directly in Workspace.

The car spawns inside a wall

It appears 12 studs in front of the pad's front face. Turn the pad toward open ground, or change the -12 in the Triggered handler.

Build it with RoCode

If you would rather describe the car than build the chassis by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the parts, constraints and scripts in your Explorer.

You type

Add a car spawner next to my spawn: a red sports car with a VehicleSeat, motor hinges on the wheels and servo steering, top speed 70, one car per player.

RoCode does
  • Builds the car and the spawner script, or makes a car model you already have drivable
  • Can read your place first to put the pad where there is open road
  • Is told to compile-check each script with Check Script, and sets undo points around each batch of changes

You still drive it yourself in a playtest to check the speed and steering feel right. How RoCode connects to Studio.

Questions

How do you make a car drive in Roblox Studio?

Join the wheels to the chassis with HingeConstraints set to Motor, weld a VehicleSeat to the chassis, and in a server script set each motor's AngularVelocity from the seat's ThrottleFloat. Steps 1 and 2 have the whole car.

Why does my Toolbox car not drive?

Free models often lose their scripts, or use old BodyVelocity and BodyGyro movers that behave badly now. Rebuild the chassis with constraints and weld the model's body onto it, as Step 4 shows.

What is a VehicleSeat in Roblox?

A seat that reads the driver's keys. It sets Throttle and Steer (whole numbers) and ThrottleFloat and SteerFloat (smooth values) that your script turns into motion.

How do I make a car spawner in Roblox?

Put a ProximityPrompt on a pad. When it fires, destroy the player's old car, build or clone a new one in front of the pad and seat them with Seat:Sit, as the CarSpawner script does.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 25 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.