SpringConstraints can be a pain to work with and don’t offer much customization. Well, in this tutorial I will be explaining everything I know about car physics and how to make it. From the very basics of suspension all the way to full control over how a car’s traction is handled.
This tutorial will be split into parts:
- Prerequisites: These are topics you should know before getting started, I will explain a little bit about them in this section, but you should do research on your own
- Explanation: These appear before the programming begins for that section. It will explain the math behind what is about to be done and how we get there
- Basics: These are the tutorials I would consider the bare minimum required to make a car system. This would be things like suspension, movement, steering, etc
- Advanced: These are the tutorials that are a bit more advanced and allow for your car system to feel more life-like. This would be things like, advanced wheel casting and throttle curves
Disclaimer: This is a tutorial aimed to teach you how to create car physics. I’ve skipped over a lot of optimization methods in favor for cleaner and easier to understand code.
I. SUSPENSION
Prerequisites
- Raycasting: Of course, you will need to know how this works because the whole system is built on the raycast. Raycasting is when you shoot out an invisible line in a certain direction and it will give you the part that intersects that line first (closest to the start of the line). Raycasts can also be limited by how far they travel so they don’t always need to travel out to infinity, we will be using this to our advantage
- Basic knowledge of CFrames: Simply need to know what CFrames are and what the difference between (CFrame * CFrame) and (CFrame + Vector3)
- Local vs World: Know the difference between local space and world space
- Algebra level math: I will do my best to explain the math used here, but it’s kinda hard to explain when you don’t know the terms (like coefficient)
- Vectors: I will explain stuff like the Dot and Cross product, but have a basic understanding of vectors
- Some Luau programming knowledge: I will explain the program, but basic stuff like syntax and programming terms I will not
Some helpful resources:
Understanding Raycasting - Resources / Community Tutorials - Developer Forum | Roblox
Suspension: Explanation
Ok, time for a lot of reading, trust me it’s worth it. Car physics in games can be simplified down to “what is below this wheel and what should happen”. The very first thing you do is use a raycast to figure out how far away from the ground is the wheel
HOOKE’S LAW
Hooke’s Law is an equation that tells you how much force a spring is applying when it is being stretched or compressed passed its rest length; that equation is F = -kx. Now if you are confused right now, that makes sense don’t worry I’ll explain.
The rest length of a spring is the distance away from its connected part where it doesn’t apply any force. So, if you look at the picture the spring on the left is at rest, we’ll say it’s 5 inches away from the roof it’s hanging on. Therefore 5 inches is its rest length, it is neither falling or moving up (assume there is no gravity).
Now what Hooke’s Law is saying is, the force (F) is equal to the stiffness of the spring (k) multiplied by its distance away from its rest length (x). That “distance away” is very important, because you want force to equal 0 at rest length. Let’s say ‘k’ actually meant “distance away from connected roof” for example. Using the same picture as before, we would plug in 5 for ‘x’ (because it’s 5 inches away from the roof). And let’s say just 1 for ‘k’ (I’ll explain stiffness in a bit). That would give us a force of 5, which doesn’t make a lot of sense. The spring isn’t moving but our equation gave us F = 5? That’s why ‘x’ means “distance away from rest length”. We will call this “displacement”.
Now stiffness (k) is a constant. The stiffness of a spring will never change it is determined by the material the spring is made of. In games, stiffness can be whatever we want, it doesn’t matter.
Finally, you may be wondering, why is it negative kx? (F = -kx). This is saying that the force a spring is applying is always opposite of the the displacement. For example, if you took a spring and stretched it to the right, then let go the spring would move to the left. It is applying a force to opposite of the way you stretched it.
MASS DAMPED SPRING EQUATION
Now Hooke’s Law is great, but in a perfect world without any outside forces (such as air resistance), our spring will be moving back and forth and back and forth forever. This is where the mass damped spring equation helps us out. Yes, it’s a terrible name.
The equation goes like this: You start off with Hooke’s Law F = -kx then we need to know how much to subtract this by so the spring isn’t looping forever, damping is what does that. Damping is a force that opposes movement so, imagine you have a pool of water, and you punch move your hand through it underwater. That’s a bit harder to do that doing it outside the water. Now replace that water with honey, honey is thick and doesn’t want to move, it’s a lot harder now. If you try moving your hand faster the more honey or water will resist. This is how we figure out the damping equation. Now going back, the liquid made you move slower, faster you went; that is velocity. The spring force, F = -kx is a force depends on position, while the damping force depends on velocity. And just like the spring force, the damping force needs to act against velocity. This gives you the equation F = -cv where ‘v’ is the velocity and ‘c’ is the damping constant (which is the same as the stiffness constant, so we can set this to whatever we want).
Before we can combine these two functions, let’s talk a bit more about the ‘v’ in F = -cv. This is a velocity along the axis of the spring. That’s a lot of words so here’s the breakdown. A velocity along an axis is just a number, imagine a number line with 0 in the middle, negatives on the left, and positives on the right. Now we have a circle moving left and right on that number line, the velocity along the number line of that circle would be a positive number when moving right, and a negative number when moving left. If that doesn’t make a whole lot of sense, here’s a gif

As you can see, is it positive when going right and negative when going left
Now that we have all of this, we can combine the two equations. We get F = -kx - cv, this is our damped spring equation. But, look at the title of this section it says mass damped spring equation. For our case, we will be assuming that the mass of every wheel is 1. If you really want to add a mass to your wheels, do this: Use Newton’s second law, which is F = ma force = mass * acceleration. Your force would be from F = -kx - cv you are solving for acceleration, so
a = F/m = (-kx - cv)/m (can’t do fraction bars, sorry). This does come with some headaches; in this tutorial we will be applying forces not acceleration so you will have to figure out a way to apply this to the car yourself.
Now a quick review. F = -kx - cv is the damped spring equation, where ‘k’ is the stiffness constant, ‘x’ is the distance away from rest, ‘c’ is the damping constant, and ‘v’ is the velocity along the spring’s axis.
Why know this?
Ok, so I went on a long tangent of where the spring and damping forces come from. Now why do we even need to know this? The simple answer is it keeps the car from slamming on the ground. We are simulating suspension and suspension is all about the spring. After raycasting you plug in the displacement (remember not distance) into the equation and that is how much force you need to apply in order to keep the car from breaking.
Final thing we need to know before starting to program, forces have a direction. The force we calculate will just be a number but to apply that back to the wheel we need to convert it into a vector. The force must always be along the spring’s axis otherwise it will get all messed up and probably fling itself.
That is everything about springs for game design (in our case)
Suspension: Programming
Now the fun part begins, programming!
THE RIG
Rigging is the most annoying part of any programming project. A rig (in our case) is just the model you want to use to act as the car. There’s a few things you want to keep in mind:
- The front face of the base part of the rig must be facing the front of the car: Lot of words, this means that the front face of the part where your “springs” would be attached should be facing towards the front of the model
- Mass matters: The bigger your car is, the more force it needs to stay up. But at the same time, if you make everything massless, physics likes to bug out (especially Roblox physics). For this tutorial, it doesn’t matter but if you are combining this with Roblox’s constraints, then it does matter
Now let’s set up our amazing, perfect looking, no modeler can do better, car.
Isn’t she beautiful! All jokes aside, to keep things simple I won’t be using any model for this tutorial so you can easily see what is going on. The program we are about to write is modular, meaning you can put this on anything (as long as you set up the rig right)
If you want to follow me exactly, the dimensions are 7x1x13.5, with no rotation
Let’s add the wheels, we will be using 4 wheels here one in each corner.
You can make these wheels as big or as small (probably don’t go below half a stud) as you want. The position of the wheels also doesn’t really matter because later on we will be setting those positions through a script. You do not need to name the wheels, if you want that’s fine, but in the script it will be looking for parts not names.
For the exacts each wheel is 0.5x3x3 at a rotation of (90, 0, 0)
Now take everything you just done and put it in a model. This is just to make sure everything stays together, and you can easily move it around. Make sure to set up a folder for wheels and name your base part correctly.
Very simple. Finally, double check that everything is unanchored.
PROGRAMMING SETUP
Here we go. Create a new Script under the car’s model and name it CarController. At the very top we need to define some constants. From before, remember we need to know a stiffness constant and damping constant. These can be whatever and will have to be tweaked after we test, but for now let’s use 10 for stiffness and 1 for damping.
A good rule to go by is the rule of 10 (not what it’s called but I call it that) which means damping should be 10x less than stiffness
local STIFFNESS = 10
local DAMPING = 1
We also need to know the rest length of the springs. This is used later to figure out what the displacement is for F = -kx. I will use a rest length of 2 which means 2 studs from the center of the base part is the resting length of the spring.
local STIFFNESS = 10
local DAMPING = 1
local REST_LENGTH = 2
And finally, we need the radius of the wheel. If you don’t know what a radius is, it’s just the distance from the center of the wheel to the edge. To find this out, look back at your wheel part and look at the Size property. Whatever one is the largest divided by 2 is your radius, simple. In my case it’s ‘1.5’.
local STIFFNESS = 10
local DAMPING = 1
local REST_LENGTH = 2
local WHEEL_RADIUS = 1.5
Now with the basics out of the way, we need a function that is very useful. That is the “getVelocityAtPoint” function. For whatever reason, Roblox’s built in GetVelocityAtPosition function never works from my experience, so this function does the same but also let’s you know how the function is doing it. I will first show you it, then explain
local STIFFNESS = 10
local DAMPING = 1
local REST_LENGTH = 2
local WHEEL_RADIUS = 3
local function getVelocityAtPoint(part: BasePart, worldPoint: Vector3)
return part.AssemblyLinearVelocity + part.AssemblyAngularVelocity:Cross(worldPoint - part.Position)
end
Yes, this looks confusing, especially if you have never taken physics. I’ll break it down, no matter what wherever you are on the part the velocity there has the current linear velocity of it. This is what part.AssemblyLinearVelocity is, it is the linear velocity of the part. Now what is this confusing mess that comes after part.AssemblyAngularVelocity:Cross(worldPoint - part.Position). part.AssemblyAngularVelocity is the current angular velocity of the part. Angular velocity is how fast the part is spinning basically. worldPoint - part.Position converts the world space point of “worldPoint” into a local space point relative to the center of part. the :Cross() is saying take the angular velocity and do the cross product on the relative point. Without taking a long time to explain what the cross product is, this gives us the additional linear velocity at that current point
Now why does that happen? Well, when something is spinning the parts of it which are further from the center have to move faster in order to keep up with the center. So, like a tire’s center moves slower (actually the center doesn’t move at all because it’s the center, but let’s say a point close to the center) than a point on the edge of the tire because of the extra distance it needs to make a full rotation.
The velocities have Assembly in front because that means it will add on the velocity of the entire assembly with it. An assembly is anything welded (either through a WeldConstraint or Weld) to this part. It doesn’t even need to be directly welded to it, this part could be welded to some part which is welded to another part
This function will be used when we are calculating the damping force. This is what is plugged into the ‘v’ in F = -cv. Well, after we of course make it along the spring’s axis.
SETTING UP THE WHEELS
Now you may have noticed when we were making our rig, it’s just parts. There’s nothing to move it, yeah we could manually move it by setting the position of every part, but that’s not a good idea. We will instead be using VectorForces these will keep our car in the air. To set this up, we will do this in code since it makes the rigging process suck less.
We will define a new function called initWheel this will be ran for every wheel in our car.
local model = script.Parent
local STIFFNESS = 10
local DAMPING = 1
local REST_LENGTH = 2
local WHEEL_RADIUS = 1.5
local function getVelocityAtPoint(part: BasePart, worldPoint: Vector3)
return part.AssemblyLinearVelocity + part.AssemblyAngularVelocity:Cross(worldPoint - part.Position)
end
local function initWheel(wheel: BasePart)
end
for _, v in pairs(model.Wheels:GetChildren()) do
if v:IsA("BasePart") then
initWheel(v)
end
end
I just typed a lot of stuff; I’ll go through it one by one.
First, we needed a reference to the car’s model, so I put that at the very top.
Next, I created our initWheel function and made a loop below it that will loop through every wheel, make sure they are BaseParts, then call the initWheel function for each.
You may have noticed when I put parameters into functions, they have like : BasePart and : Vector3 these are types, they are not required, but they give you intellisense (autocomplete)
For the suspension forces later on, we need a reference to the position of these wheels. Later on in the tutorial, these wheels will be moving so we can’t just use the position of the part. We are instead going to use Attachments to easily store the position
local model = script.Parent
local body = model.Body
local STIFFNESS = 10
local DAMPING = 1
local REST_LENGTH = 2
local WHEEL_RADIUS = 1.5
local wheels = {}
local function getVelocityAtPoint(part: BasePart, worldPoint: Vector3)
return part.AssemblyLinearVelocity + part.AssemblyAngularVelocity:Cross(worldPoint - part.Position)
end
local function initWheel(wheel: BasePart)
wheel.Anchored = false
wheel.CanQuery = false
wheel.CanTouch = false
wheel.CanCollide = false
local attachment = Instance.new("Attachment")
attachment.Parent = body
attachment.WorldPosition = wheel.Position
table.insert(wheels, {
attachment = attachment
})
end
for _, v in pairs(model.Wheels:GetChildren()) do
if v:IsA("BasePart") then
initWheel(v)
end
end
Quick note, it is good practice to always set the parent of a new instance you created through a script after you assigned all the properties. In our case we cannot do that since once you set the parent of an attachment, the WorldPosition gets changed, we need to make sure it’s in the right spot.
You also may have noticed I made the wheel unanchored, non collidable, non-quarriable, and not touchable. This is because the wheel part is all visual, it has nothing to do with the car. If the wheel was collidable, it would hit the floor and cause the car to bounce when it shouldn’t be bouncing.
SPRINGS!
We are finally done with setup and can move to applying all our new knowledge into code. Let’s make the function that gives us the damped spring force.
This is a two-step process:
- Calculate the velocity along the spring’s axis
- Plug into the damped spring equation
You should know how to get the velocity at the wheel’s position since was the first function we made
local function getSpringForce(worldWheelPos: Vector3, displacement: number): Vector3
-- F = -kx - cv
local velocity = getVelocityAtPoint(body, worldWheelPos)
end
As you can see, getSpringForce() takes in the world position of the wheel. When we are raycasting we will convert the local position of the wheel back to world.
Alright, now we need to get the velocity along the spring’s axis. This is done using the vector’s dot product. For this tutorial, all you need to know about the dot product is “it takes the parallel and discards the rest”. This means only the values that are parallel to the other vector in the dot product will be given.
local function getSpringForce(worldWheelPos: Vector3, displacement: number): Vector3
-- F = -kx - cv
local velocity = getVelocityAtPoint(body, worldWheelPos)
local verticalSpeed = body.CFrame:VectorToWorldSpace(Vector3.yAxis):Dot(velocity)
end
Vector3.yAxis is the spring’s axis. The spring will only ever apply a force in the UP direction so that’s the speed we need to get. Now why body.CFrame:VectorToWorldSpace()? This rotates (0, 1, 0) to be the up vector of the body. This is to ensure that the speed we are getting is only along the spring’s axis, and since the spring rotates with the body this makes sense.
Sidenote: You can do “body.CFrame.UpVector:Dot(velocity)” which means the same thing in this tutorial, but if you read the advanced section, you will know why it is done this way (it allows you to have the direction of gravity to whatever you want)
Now that we have the vertical speed, it’s as simple as plugging into the formula. For easier reading, I have separated it.
local function getSpringForce(worldWheelPos: Vector3, displacement: number): Vector3
-- F = -kx - cv
local velocity = getVelocityAtPoint(body, worldWheelPos)
local verticalSpeed = body.CFrame:VectorToWorldSpace(Vector3.yAxis):Dot(velocity)
local springForce = -STIFFNESS * displacement
local dampingForce = DAMPING * verticalSpeed
local totalForce = springForce - dampingForce
return totalForce * Vector3.yAxis
end
Simple, we multiply by Vector3.yAxis to get it back into vector form since the damped spring equation returns a number (again Vector3.yAxis is the spring’s axis).
APPLYING FORCES & RAYCASTING
It’s time to get these to actually do something.
Let’s go back to the initWheel function to add something new
local function initWheel(wheel: BasePart)
wheel.Anchored = false
wheel.CanQuery = false
wheel.CanTouch = false
wheel.CanCollide = false
local attachment = Instance.new("Attachment")
attachment.Parent = body
attachment.WorldPosition = wheel.Position
local springVectorForce = Instance.new("VectorForce")
springVectorForce.Force = Vector3.zero
springVectorForce.Attachment0 = attachment
springVectorForce.Parent = attachment
springVectorForce.Name = "Spring"
springVectorForce.RelativeTo = Enum.ActuatorRelativeTo.World
table.insert(wheels, {
attachment = attachment,
springForce = springVectorForce
})
end
We need an actual vector force to apply all the forces. This is setup very easily, we make it relative to the world, so it doesn’t rotate with the body (always apply the force up). Then, we add it to the wheels table so we can reference it later.
Now it’s time to use all this new stuff we created! We will create a function called applyForces() this will figure out the displacement of the wheel that will then be plugged into the getSpringForce() function and then apply the force.
local function applyForces(wheel: { attachment: Attachment, springForce: VectorForce })
end
So, we need to figure out how far away is the wheel from the ground. That’s a tricky problem but that’s exactly what the raycast solves. We can use a raycast and it will give us a distance from the starting point.
To set this up we need to first know where the ray should start. That’s easy just the attachment’s WorldPosition. Now we need a direction, that’s a bit more confusing. Let’s first solve the end position, where should this ray end?
Your first thought may be infinity, but we don’t want that, why? Well, let’s think logically; we have a wheel that has a simulated spring which should do its best to keep the wheels on the ground. If we were to drive the car off a ramp, our raycast will still hit the ground and apply forces. It would say that we are so far away from the ground that a lot of force needs to be applied to fix this before it gets out of hand. Well, by applying that much force, everything just got out of hand and the car has been flung.
Ok so we don’t use infinity, then what? Well, we need the force to be applied when the car is touching the ground, but not when it’s not. This is where the REST_LENGTH comes in, we take the starting position of the ray and go down the rest length of the spring. But think longer once more; are we really done? Yes, we went the rest length of the spring, but wheels are flat, they have height! We need to keep going down to fit in the height of the wheel, which so happens to be the radius of the wheel since rest length moves the center of the wheel to the floor, then we need to move the radius of the wheel to get to the bottom of the wheel.
This ends up being start + REST_LENGTH + WHEEL_RADIUS for our ending position of the ray. This isn’t what workspace:Raycast() is asking for though, it’s asking for a direction. Well, a ray’s direction can be described as pointing somewhere with a distance attached to it. So where are we pointing? Down, because we need to reach the floor. What’s the length? Well, that’s REST_LENGTH + WHEEL_RADIUS. Doing some vector math, we get a direction of DOWN * (REST_LENGTH + WHEEL_RADIUS), let’s put that into code.
local function applyForces(wheel: { attachment: Attachment, springForce: VectorForce })
local down = -wheel.attachment.WorldCFrame.UpVector
local start = wheel.attachment.WorldPosition
local direction = down * (REST_LENGTH + WHEEL_RADIUS)
end
Why did I set down to be the negative up vector of the attachment? Well, that’s because down won’t always be (0, -1, 0) the car could be on a slope and now down is like down and to the left.
There is one more thing we need for our raycast and that’s RaycastParams. Since we are starting the raycast pretty close to the body of the car, there’s a chance it could think it hit the body of the car and snap the wheel up. Using RaycastParams we can easily ignore the body of the car.
local function applyForces(wheel: { attachment: Attachment, springForce: VectorForce })
local down = -wheel.attachment.WorldCFrame.UpVector
local start = wheel.attachment.WorldPosition
local direction = down * (REST_LENGTH + WHEEL_RADIUS)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.RespectCanCollide = true
params.FilterDescendantsInstances = {model}
end
Few extra things, RespectCanCollide this is to make sure that the wheel is only driving on parts that are actually collidable, otherwise it may try driving on a wall.
FilterDescendantsInstances is set to {model} not {body} this is because we want to ignore every part in the entire car’s model not just the body.
Now that we have everything we need to do a raycast, let’s do it in code.
local function applyForces(wheel: { attachment: Attachment, springForce: VectorForce })
local down = -wheel.attachment.WorldCFrame.UpVector
local start = wheel.attachment.WorldPosition
local direction = down * (REST_LENGTH + WHEEL_RADIUS)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.RespectCanCollide = true
params.FilterDescendantsInstances = {model}
local result = workspace:Raycast(start, direction, params)
if result ~= nil then
local distance = result.Distance
end
end
The RaycastResult gives us a Distance which is the distance from the start of the ray to the hit. We need displacement though. Displacement again is the distance away from rest length and this does have a sign so above and below would be negative and positive.
We can easily get this by first making sure we are in the center of the wheel, which is where the spring is attached to.
local distance = result.Distance
local length = distance - WHEEL_RADIUS
Then, we simply figure out the offset
local distance = result.Distance
local length = distance - WHEEL_RADIUS
local displacement = REST_LENGTH - length
That’s it!
Now that we have displacement we can finally figure out how much force is needed to be applied. We can call getSpringForce() and it will return our force.
local function applyForces(wheel: { attachment: Attachment, springForce: VectorForce })
local down = -wheel.attachment.WorldCFrame.UpVector
local start = wheel.attachment.WorldPosition
local direction = down * (REST_LENGTH + WHEEL_RADIUS)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.RespectCanCollide = true
params.FilterDescendantsInstances = {model}
local result = workspace:Raycast(start, direction, params)
if result ~= nil then
local distance = result.Distance
local length = distance - WHEEL_RADIUS
local displacement = REST_LENGTH - length
local springForce = getSpringForce(wheel.attachment.WorldPosition, displacement)
end
end
Now we can use our VectorForce we created to apply that force onto the car.
local springForce = getSpringForce(wheel.attachment.WorldPosition, displacement)
wheel.springForce.Force = springForce
And we’re done! Or are we? (vsauce) Remember how I mentioned the car going off the ramp. Right now, we our VectorForce will keep applying whatever force its set to, it doesn’t know when the car is off the floor. We need to make sure we reset the force back to (0, 0, 0) otherwise the car will float away. That’s super simple:
local function applyForces(wheel: { attachment: Attachment, springForce: VectorForce })
local down = -wheel.attachment.WorldCFrame.UpVector
local start = wheel.attachment.WorldPosition
local direction = down * (REST_LENGTH + WHEEL_RADIUS)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.RespectCanCollide = true
params.FilterDescendantsInstances = {model}
local result = workspace:Raycast(start, direction, params)
if result ~= nil then
local distance = result.Distance
local length = distance - WHEEL_RADIUS
local displacement = REST_LENGTH - length
local springForce = getSpringForce(wheel.attachment.WorldPosition, displacement)
wheel.springForce.Force = springForce
else
wheel.springForce.Force = Vector3.zero
end
end
Now we’re done. That was a lot I know so here’s a quick recap:
- We built our damped spring equation into code
- We figured out how to calculate the displacement using raycasts
- Worked out logically how to find the direction that is needed for the raycast
- Then used all of that to apply a force using a VectorForce back onto the car
MAKING IT RUN
For the final section, we need to make this actually run. Right now, it’s just a bunch of functions disconnected from each other. We need a worker calling all of these, that worker is RunService
At the top of your code, include RunService local RunService = game:GetService("RunService") then at the very bottom, connect to the Heartbeat
RunService.Heartbeat:Connect(function(dt: number)
end)
We are connecting to the Heartbeat because this runs after the physics simulation. This just makes everything more stable. All we need to do here is loop through all the wheels and call applyForces()
RunService.Heartbeat:Connect(function(dt: number)
for _, wheel in pairs(wheels) do
applyForces(wheel)
end
end)
Notice how I’m looping through the wheels table and not model.Wheels:GetChildren()), that’s because we need to have access to the attachment and spring force (without having to search for it).
TESTING & DEBUGGING
You can now hit play and watch as all your hard work… fails immediately, so be the law of a programmer. The car is just stuck on the floor. Don’t panic, let’s get into debugging mode.
First let’s figure out why the car is stuck on the floor. Roblox has a nice handy tool that lets you see constraints, we can toggle this on at Model -> Constraints -> Constraint Details you should be seeing blue arrows at the corners of your car (if not go back up and make sure you didn’t miss a step).
Click on one of the arrows and look at the properties, what is the force? If you are seeing like 30, 40, 60 something very small then that is because our springs aren’t strong enough. They need to be stiffer or in other words, have a larger stiffness. I’ll save you the trouble of tweaking stiffness and damping since I’ve already done it. If you are using the same set up as me, your stiffness should be at 2500 and damping at 250. Yeah, those are a lot bigger than before, you just need to tweak and see what happens.
Now hit play again, you should now see it working right! Nope! The car really wants to smash through the floor. So, again let’s turn on constraint details and see what’s happening. The way the arrow is pointing is the direction the VectorForce is applying the force.
Here it’s pointing down which is completely incorrect, your first thought might be to just negate the direction of the spring force. So, let’s do that and see what happens
local function getSpringForce(worldWheelPos: Vector3, displacement: number): Vector3
-- F = -kx - cv
local velocity = getVelocityAtPoint(body, worldWheelPos)
local verticalSpeed = body.CFrame:VectorToWorldSpace(Vector3.yAxis):Dot(velocity)
local springForce = -STIFFNESS * displacement
local dampingForce = DAMPING * verticalSpeed
local totalForce = springForce - dampingForce
return -totalForce * Vector3.yAxis
end
So, I negated the totalForce, it’s now -totalForce * Vector3.yAxis instead of totalForce * Vector3.yAxis
Hit play again and watch the car ascend to the heavens or more like bounce to the heavens. Now the spring force isn’t being dampened. So, the problem is not the damping force and not the total force, it’s actually our spring force. Remember how spring force is equal to -kx well, the negative is the problem for us. This is causing the spring force to apply in the wrong direction, simply get rid of that (and remove the negation from total force) and everything should be good.
local function getSpringForce(worldWheelPos: Vector3, displacement: number): Vector3
-- F = kx - cv
local velocity = getVelocityAtPoint(body, worldWheelPos)
local verticalSpeed = body.CFrame:VectorToWorldSpace(Vector3.yAxis):Dot(velocity)
local springForce = STIFFNESS * displacement
local dampingForce = DAMPING * verticalSpeed
local totalForce = springForce - dampingForce
return totalForce * Vector3.yAxis
end
If everything goes well, it should look like this
You will find how to make the wheels actually follow the car in the advanced section (so it isn’t just a floating brick)
If you don’t like how low it is to the ground, simply increase the REST_LENGTH and it will raise it.
Final Code
local RunService = game:GetService("RunService")
local model = script.Parent
local body = model.Body
local STIFFNESS = 2500
local DAMPING = 250
local REST_LENGTH = 2
local WHEEL_RADIUS = 1.5
local wheels = {}
local function getVelocityAtPoint(part: BasePart, worldPoint: Vector3)
return part.AssemblyLinearVelocity + part.AssemblyAngularVelocity:Cross(worldPoint - part.Position)
end
local function getSpringForce(worldWheelPos: Vector3, displacement: number): Vector3
-- F = kx - cv
local velocity = getVelocityAtPoint(body, worldWheelPos)
local verticalSpeed = body.CFrame:VectorToWorldSpace(Vector3.yAxis):Dot(velocity)
local springForce = STIFFNESS * displacement
local dampingForce = DAMPING * verticalSpeed
local totalForce = springForce - dampingForce
return totalForce * Vector3.yAxis
end
local function initWheel(wheel: BasePart)
wheel.Anchored = false
wheel.CanQuery = false
wheel.CanTouch = false
wheel.CanCollide = false
local attachment = Instance.new("Attachment")
attachment.Parent = body
attachment.WorldPosition = wheel.Position
local springVectorForce = Instance.new("VectorForce")
springVectorForce.Force = Vector3.zero
springVectorForce.Attachment0 = attachment
springVectorForce.Parent = attachment
springVectorForce.Name = "Spring"
springVectorForce.RelativeTo = Enum.ActuatorRelativeTo.World
table.insert(wheels, {
attachment = attachment,
springForce = springVectorForce
})
end
local function applyForces(wheel: { attachment: Attachment, springForce: VectorForce })
local down = -wheel.attachment.WorldCFrame.UpVector
local start = wheel.attachment.WorldPosition
local direction = down * (REST_LENGTH + WHEEL_RADIUS)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.RespectCanCollide = true
params.FilterDescendantsInstances = {model}
local result = workspace:Raycast(start, direction, params)
if result ~= nil then
local distance = result.Distance
local length = distance - WHEEL_RADIUS
local displacement = REST_LENGTH - length
local springForce = getSpringForce(wheel.attachment.WorldPosition, displacement)
wheel.springForce.Force = springForce
else
wheel.springForce.Force = Vector3.zero
end
end
for _, v in pairs(model.Wheels:GetChildren()) do
if v:IsA("BasePart") then
initWheel(v)
end
end
RunService.Heartbeat:Connect(function(dt: number)
for _, wheel in pairs(wheels) do
applyForces(wheel)
end
end)
Due to the body character limit, each tutorial after (either basic or advanced) will have its own message in this post
It’s recommended you go through the tutorials in order, wheel casting is technically the 5th tutorial but in reality it can be added to any tutorial, it’s very standalone
BASICS
II. Wheels Following Suspension
III. Movement & Basic Friction
IV. Steering & Rolling Friction
ADVANCED
V. Wheel Casting – WARNING There is an error in this tutorial, the correction can be found here
VI. Client Integration
VII. Traction Control & Advanced Friction: TODO
VIII. Multiplayer Integration: TODO
IX. Multiple Vehicles & Modular Setup: TODO
X. Multiple Bodies: TODO
XI. Optimizations & Using in Real Games: TODO
If there are any typos or mistakes throughout the tutorial, please tell me. And of course, if you need help, don’t be afraid to ask
If you want the .rbxm file, here is it. This includes all the full scripts (with comments) of every tutorial
car tutorial.rbxl (90.2 KB)










