GDSim v0.4a - Autocross and Custom Setups

  • Thread starter Wolfe
  • 62 comments
  • 29,678 views

Wolfe

Physics Critic
Premium
13,549
United States
Wisconsin
My simulator prototype, GDSim, is now available on Itch.io as a free download for Windows, Linux, and MacOS!



Welcome to my workshop/devlog. Over the past year I have been learning how to use Godot, a free and open-source game development platform, and its scripting language, GDScript, to make my very own driving simulation. This thread is a compilation of resources I found and things I have learned along the way. Some things were hard to find, and some URLs are very old, at risk of disappearing. For the most part, what you need to know consists of "off the shelf" formulas and ways to apply them.

The vision for this project has evolved a bit since October. If you've seen it before you'll see I've rewritten this OP. My current plan is to update GDSim as I continue developing my simulation. It will serve as a demonstration of my progress on the simulation itself, similar to the demo version of Live for Speed. I will finish the workshop series with the topics I had planned, and then I could post updates when I have completed something new.

Sooner or later, I want to begin working on a more elaborate project under a different name. Godot's developers have been teasing version 4.0 for a long time, but when that milestone arrives it will be more suitable for a larger project. Faster, more streamlined, and even more versatile. For now I think it will pay off to focus on depth rather than breadth.

In the meantime, please feel free to comment or ask questions. :)
 
Last edited:
Post Index:
I. Suspending a RigidBody
II. Suspending a RigidBody, Part 2
III. Rays Go Round - Lateral Force
IV. Rays Go Round - Adding Things Up
V. Start Your Engines
VI. Brushing Up and Misc. Details


General Formulas:
Hooke's Law for springs:
spring_force = stiffness * compression

Damping factor added to Hooke's Law:
spring_force += damping * (compression - previous_compression) / delta

Approximation of Ackermann Steering (negative "Ackermann" value for right wheel):
steering_angle = max_steering_angle * (input + (1 - cos(input * 0.5 * PI)) * ackermann)
Slip Ratio:
slip_ratio = (angular_velocity * radius - velocity) / velocity_magnitude

Pacejka "Magic" Tire Formula:
Force = Load * Peak * sin(Shape * arctan(Stiffness * Slip - Curvature * (Stiffness * Slip - arctan(Stiffness * Slip))))

Rolling Resistance:
roll_resist = 0.005 + (1 / pressure) * (0.01 + 0.0095 * (velocity / 100)^2)
Torque and the Moment of Inertia:
torque = moment_of_inertia * acceleration
acceleration = Torque / moment_of_inertia = delta_velocity / delta_time
delta_velocity = delta_time * torque / moment_of_inertia

Moment of Inertia for a Solid Disc:
moment_of_inertia = 0.5 * mass * radius^2


Sources:
How to Build a Racing Game (Jake Gordon) -- Not super relevant here, but offered if you'd like to start where I started. :D
Car Physics for Games (Marco Monster) -- A seminal source. Excellent place to start...so long as it remains online. (Check out the date!)
Space Dust Racing UE4 Arcade Vehicle Physics Tour (Space Dust Studios) -- Video overview of the raycast suspension concept.
Pacejka '94 Parameters Explained (Edy) -- Deep dive into Pacejka parameters, with a collection of more- and less-helpful example values.
Tyre Dynamics (Racecar Engineering) -- Overview of fundamental concepts in tires.
Unity Drift Simulator Tutorial (NoCakeNoCode) -- A comprehensive example in Unity's programming language, which inspired my drivetrain simulation
Combined Slip Tire Models (Imam) -- MATLAB scripts for both tire models discussed here
 
Last edited:
I. Suspending a RigidBody

I first thought I might make this thread a more detailed tutorial, but that is too much for me to commit to, so in the interest of brevity, this thread will be more like "Car Physics for Games" (link above), explaining the necessary concepts and math in a way that I hope will be helpful, and tricks I learned along the way. I would like to explain how I built a simulator in Godot rather than how to use Godot. If you're interested in learning Godot, I recommend taking a look at the documentation site, or any other tutorial you like. For a couple alternatives, I found this website helpful, and KidsCanCode is another I see passed around.

If you have used a 3D game making program like Unity, this stuff will probably be familiar. If you prefer using Unity or UE4 or whatever, much of it (all of it?) should still be applicable.

So please forgive me for zipping right into it -- how to suspend a RigidBody?

The first solution I tried is apparently a common first attempt, and not really the ideal solution -- what is called a constraint-based model. I had a RigidBody for the car and four more RigidBodies for the wheels, suspended together with Joint nodes. It was promising at first, but didn't pan out. You can get regular friction-based traction and steering and all for low-speed physics -- and even add a tire formula on top -- but no matter what values or tricks I tried, it was unstable and unpredictable. I was spending more time fighting with the physics engine than anything else, so I went back to the drawing board.

A raycast-type suspension is a proven solution, but I think I found something even better than a regular raycast -- Godot's SpringArm node. Not only does it cast a ray, it will automatically move its children nodes to the position where the collision took place, which is useful for positioning the wheel mesh (with zero code!). It is intended for camera control, where a third person camera "springs" out while sliding against walls so that it will not clip through them. Its standard orientation points to Z+ (where a camera would be), so it must be rotated to point down. Unfortunately, this means Z becomes Y and Y becomes Z in all its vector calculations.

What makes the SpringArm extra handy is that you can cast a shape; this avoids issues with using a single raycast model, while remaining simple. I've got it casting a sphere out, with a radius matching the wheel radius. (I haven't found a way to make it cast a cylinder in the correct orientation.)

The node's "Spring Length" value is the maximum suspension travel; position the SpringArm where the wheel would be when the suspension is fully compressed. If you've added the mesh already, it will help you find the right position.

The SpringArm node can report its compression ratio with "1 - (get_hit_length() / spring_length)", where 1 is fully compressed and 0 is fully extended. get_hit_length() is the result of the raycast (in meters). Then I apply Hooke's Law:
spring_force = stiffness * compression

Since I've measured compression as a ratio, it is multiplied by the SpringArm's spring_length:
Code:
   var compress = 1 - (get_hit_length() / spring_length)  # 1 is fully compressed
   y_force = spring * compress * spring_length

Next is damping, which relates to the velocity of the suspension movement, so you multiply the damping value by the change in compression divided by time. All you have to do is record the previous compression ratio for each physics frame:
damping = damp_value * (compression_ratio - prev_compression_ratio) / delta

If you check the difference between the last compression ratio and new compression ratio, you can determine whether the spring is being compressed or released, which allows you to implement bump damping and rebound damping separately. :)
Code:
if (compress - prev_compress) >= 0:  y_force += bump * (compress - prev_compress) / delta
else:  y_force += rebound * (compress - prev_compress) / delta

All that is left is to apply the calculated force to the RigidBody from each SpringArm at each corner. If you're familiar with working in 3D, you should know how to get the relative position -- but I have done it a little bit differently. I will explain in a second post.
 
Last edited:
II. Suspending a RigidBody, Part 2

The last post is kind of an incomplete concept, so right away today I'm going to share the second piece to the puzzle that really helps the SpringArm-based shapecasting work. Instead of comparing the position of the SpringArm and RigidBody to locate the applied forces, I've added an extra node as a child of the SpringArm -- a plain RayCast.

Why a RayCast together with the SpringArm? Two important reasons -- the SpringArm (as of this writing) is unable to report anything about the surface it collided with, only how much it is compressed, and a wheel is (obviously) not a sphere. If we apply forces any time the sphere is in contact, rolling the car will usually result in a sudden spike in force when the SpringArm is suddenly compressed, with amusingly terrible results. :lol:

By adding a RayCast as a child of the SpringArm, pointing straight out of the bottom of the sphere (make sure it doesn't point forward due to the SpringArm's rotation), you can make sure the bottom of the sphere is in contact before actually applying forces. I'm finding a single RayCast to work well enough. You can also collect the point of contact on the surface with "$RayCast.get_collision_point()", and find out what the wheel is rolling over with "$RayCast.get_collider()".

Lastly, it's a good idea to collect the normal vector of the collision with "$RayCast.get_collision_normal()". I've used this to orient the application of forces relative to the surface, instead of the orientation of the SpringArm or RigidBody.

I've basically done everything you would do with a raycast by itself, except for the suspension force, but with this combo I can take advantage of a round collision shape.

So. For just the suspension, I've applied the force calculated in part one (called "y_force") like this:
Code:
    var contact = $RayCast.get_collision_point() - car.global_transform.origin
    var normal = $RayCast.get_collision_normal()
 
    if $RayCast.is_colliding():  # Check helps eliminate force spikes when tumbling
        car.add_force(normal * y_force, contact)

I'm able to just write "car" because at the top of the script I defined that variable to represent the parent RigidBody of the SpringArm for readability:
Code:
onready var car = $'..'    # parent node

This is enough to drop a RigidBody over four SpringArms and watch the suspension work, similar to this point in the Space Dust Racing video.


For the time being, I'm going to get back to working on my clutch simulation. The drivetrain has been the most complicated part. :boggled:
 
Last edited:
I had good luck with my clutch simulation. :) It's not totally accurate -- clutch slip and its effect on the engine are ultimately based on clutch pedal position, not torques -- but it is working well enough. You can now:
  • Slip the clutch to set off
  • Spin the drivewheels from a start (notable improvement over the example I had followed before)
  • Stall the engine, or turn the ignition off (just because)
  • Bump-start the engine, or restart it with input (I've arranged it with flooring the throttle, separate from ignition, no starter button required)
Getting bump-starting to work was fun! Once I got the fundamental clutch model to work, it was relatively easy to add an auto-clutch, which disables stalling (and ignition control, at least for now) and approximates aggressive clutch slip for effective launches. With the auto-clutch behavior in place, I implemented an automatic transmission -- as in an automatic with basic automatic-like shift logic, not a manual that shifts itself.

On the short term to-do list is a suitable test track/area worth driving around in, so I can start to hone in on reasonable values for the tire model, which is on the slippery side right now. If that works out, the plan is that an upload of some kind won't be far behind.


III. Rays Go Round - Lateral Force

Now that we have a suspension, it's time to simulate some rubber beneath it. Before you can do that, there is some information to keep track of:
  1. The wheel's "local" velocity
  2. The wheel's "Z velocity", which is just the forward/back component of its local velocity (I have reasons to store this separately)
  3. What I call the wheel's "planar vector" -- a convenient normalized/unit vector that excludes vertical motion
  4. The wheel's last position, because the SpringArm is not a physics object and Godot does not calculate its velocity automatically
To get these, we do a little bit of vector math. If you're lost, don't feel bad -- I had to reacquaint myself. :lol: This "cheat sheet" might help. I've collected the variables like this:
Code:
    var local_vel = global_transform.basis.xform_inv((global_transform.origin - prev_pos) / delta)
    z_vel = -local_vel.y
    var planar_vect = Vector2(local_vel.x, local_vel.y).normalized()
    prev_pos = global_transform.origin
Note that the 'Z' velocity is the 'Y' component of the full vector. That's one reason I give it another variable -- for clarity -- and a consequence of rotating the SpringArm node, as I mentioned in part one. Negative Z is forward in Godot, so we pull it from negative Y here.

As I've mentioned, I have a "brush" tire formula I'm using, but I think it would be more informative to start with a simplified Pacejka formula first -- which is what I started out with. The Pacejka formula can be easily plotted to a graph, which will help explain how tires work and what we're tinkering with.

"Pacejka?" -- The Pacejka formula is an equation concocted to fit empirical data collected from testing real tires. It is named for Hans Pacejka, one of the authors of the formula. A sim developer called Edy, who sells a physics package in the Unity asset store, provides a helpful page going into depth on the 1994 version of the Pacejka formula as well as a overview of the model. In short, it is an effective but unwieldy formula to use, with little guidance on good parameters.

To make use of any tire formula -- it doesn't have to be Pacejka or the brush formula -- we have a bit more necessary information to collect: the tire's slip angle and slip ratio.

Slip Angle
I suppose many GTPlanet members will be familiar with slip angle, but don't worry if you're not -- it is the angle between the direction the tire is pointing and the direction it's rolling. I have another link here for reference. :)

There are multiple ways to derive the slip angle. My method makes use of the "planar vector" from the beginning. I've called this variable "x_slip" to denote lateral slip:
Code:
var x_slip = asin(clamp(-planar_vect.x, -1, 1))

That's all. The arcsine of the (negative) x component of the vector, with a check to make sure its floating point value doesn't slightly exceed -1 or 1, which is undefined in an arcsine function (oops).

Slip Ratio
The slip ratio compares the angular velocity of the wheel (I call it "spin" for short) with how quickly the road is passing by beneath it -- to put it simply, wheelspin or locking the wheels. There are multiple definitions for the slip ratio; I'm using one where locking the brakes equates to -1 if you're moving forward, and +1 if you're moving backward. This time we have to avert dividing by zero:
Code:
var z_slip = 0
if not z_vel == 0:  z_slip = (spin * radius - z_vel) / abs(z_vel)
"Spin" must be tracked manually -- in fact, doing so is more useful than using a built-in angular velocity value on a physics object -- and it is modified elsewhere. See section IV below for calculating it.

Simplified Pacejka
Now it's time for the formula. You can actually get a half-decent approximation by skipping most of the parameters. The formula goes like this in English:
Force = Load * Peak * sin(Shape * arctan(Stiffness * Slip - Curvature * (Stiffness * Slip - arctan(Stiffness * Slip))))

...where "load" is the acting weight on the tire from the car (our "y_force" from part one), in Newtons, giving a force in Newtons (with the lateral slip angle in radians).

This is where it becomes handy to plot the formula to a graph (and where things start to get complicated). I used a nifty graphing calculator available on Desmos.com. (There, I've used the standard coefficient labels because of how the calculator works.)

Peak (D) = Simply determines the peak of the curve; comparable to coefficient of friction, I think? (example: 1)
Shape (C) = Affects the shape of the curve in the middle, after it has passed the peak (sources say ~1.35 for lateral force, ~1.65 for longitudinal force)
Stiffness (B) = Affects the slope of the curve around the origin (example: 10)
Curvature (E) = Affects the height of the tail of the curve, together with "shape" (example: 0, going negative from there)

If you haven't seen this curve before, it is a reasonably accurate approximation of the characteristics of a tire. As slip increases, there is an amount of slip that will provide the most force. To us drivers, generally speaking, if the curve is pointy and peaky, it will make for a "twitchy" tire that suddenly breaks away. If it is round and broad, the tire will be more forgiving. The lower the tail, the less traction you have once you've lost grip. The higher the peak, the more grip you get...at the peak.

I used Godot's "export" variables to make sliders for the parameters that can be modified in the Godot editor window while testing ("Always on top" in the window settings for rendering is nice). Something like this -- I honestly don't know if it is better to have separated values for all four coefficients:
Code:
export(float, 0, 2, 0.05) var peak = 1
export(float, 1, 2.5, 0.05) var x_shape = 1.35
export(float, 1, 2.5, 0.05) var z_shape = 1.65
export(float, 0, 20, 0.1) var stiff = 10
export(float, -10, 0, 0.05) var curve = 0

Then I created a method just for the formula:
Code:
func pacejka(slip, t_shape):
    return y_force * peak * sin(t_shape * atan(stiff * slip - curve * (stiff * slip - atan(stiff * slip))))
I have to use "t_shape" instead of just "shape" ('t' for tire) because SpringArm already has a "shape" property.

Now there's enough in place to apply some force via the tires. I figure it makes sense to implement the lateral force first -- add it where we applied "y_force" before. (Note: this example will use the X basis; I'll show how I rotated the surface normal when I bring things around to the brush formula, because I'm drawing from different stages of progress and don't want to mix up +/-):
Code:
x_force = pacejka(x_slip, x_shape)
z_force = pacejka(z_slip, z_shape)

if $RayCast.is_colliding():  # Check helps eliminate force spikes when tumbling
   car.add_force(global_transform.basis.x * x_force, contact)
   car.add_force(normal * y_force, contact)
   car.add_force(global_transform.basis.x * z_force, contact) ## See note in post below
This should be enough to shove the car around and let grip align it with its wheels (if the car spins like a top, chuck a negative sign in there). Without longitudinal force, it will basically act like it is always freewheeling. Otherwise, it will act like the brakes are all locked (so far). There's more to do before the longitudinal force can be properly implemented. That was one hard part to figure out, and this post is long enough.
 
Last edited:
Alright, one more thread bump so I don't have the nagging feeling of leaving it hanging with only lateral force. The part after might wait until a bit later.

Since the last post I've fleshed out the transmission simulation a bit more and started preparing some elements for making a marginally presentable demo.

IV. Rays Go Round - Adding Things Up

In "Car Physics for Games", Marco describes a simplistic way of applying torque and accelerating a car, then reading the new RPM back from the wheels. He also briefly outlines a more advanced way of doing it, which is to sum up the torques acting upon the wheels and keep track of the wheel's angular velocity as a result of these competing forces. I think I've developed a pretty solid way of doing that, while avoiding the common consequence of feedback effects in physics-based game design.

The way I've organized my project is to have everything run in one _physics_process() from the RigidBody, so I can make sure all four wheels are updated at once and everything is done in order. All this suspension and tire stuff gets called in a custom method. The RigidBody also passes inputs (from a third node that collects them, for convenience) and simulates the powertrain. Once everything is ready, the RigidBody calls another method on the SpringArm to add up the torques from the engine, tire friction, and braking.

My reasoning: you can initialize the torque for the current frame with friction from your tire model, then add torque from the drivetrain, and finally apply braking torque against those. If braking torque exceeds the other two and the wheel's angular velocity is already almost zero, I just stop it. This mitigates "fidgeting" on stopped wheels from competing forces, so long as the user is on the brakes.

Here is where we calculate "spin" for the slip ratio, and complete the basic necessities for a tire model.

The first step is easy. The longitudinal tire force ("z_force") multiplied by the wheel radius gives a torque. To that, you can go ahead and add the torque from the engine:
Code:
   var net_torque = z_force * radius
   net_torque += drive_torque
For drive torque, you can use a static value multiplied by the throttle input to get started.

Applying the brake torque was a little trickier. I mentioned the part about stopping the wheel (first line); the other part is keeping it from spinning the wheels in reverse! :) To do that, I multiply the brake torque by the sign of "spin". Again, a static value multiplied by input will suffice to start with:
Code:
   if abs(spin) < 5 and brake_torque > abs(net_torque):  spin = 0
   else:
       net_torque -= brake_torque * sign(spin)
       spin += delta * net_torque / wheel_moment
Now what's going on in that last line? That is the relationship of angular velocity with torque:
torque = moment_of_inertia * acceleration
acceleration = Torque / moment_of_inertia = delta_velocity / delta_time

delta_velocity = delta_time * torque / moment_of_inertia

The wheel's moment of inertia (rotational inertia) is calculated from its mass (in kilograms), with an export variable to tweak the mass and a quick calculation in _ready() at the start. A simple moment of inertia for a solid disc-shaped object is 0.5 * mass * radius^2, which is close enough for our purposes:
Code:
wheel_moment = 0.5 * wheel_mass * radius * radius

With that, the wheels can have some mass to them and react to friction, throttle input, and brake input. To confirm, animate the angular velocity upon your wheel mesh. :) This part can go into an ordinary _process() thread because it's only visual:
Code:
$Mesh.rotate_x(-spin * delta)

Ackermann Steering
Now's about the time to implement steering, so I'll share my simple formula for an approximation of Ackermann steering, given a negative Ackermann variable for the front right wheel:
Code:
func steer(input, max_steer, ackermann):
   rotation.y = max_steer * (input + (1 - cos(input * 0.5 * PI)) * ackermann)
Adjust the variable to suit whichever model you're using. In my case for now, it is 0.15.

That's pretty much the basic core of the model! I've trimmed some details out to make it easier to follow, piece by piece. I don't know how successful I've been in that objective.

Next time I can share an example I followed that helped me find an effective way to update the engine RPM.
 
Last edited:
It was just today I learned of this project. Back when I was more active with Godot Game Engine (highly recommend- very easy), I devised a racing/driving game development system called "Project Rhiannon." I had cycled back and forth between treating the player's vehicle like a proper vehicle or more like a platformer character. Godot is not very solid in regards to a proper model for vehicle physics. However, and since my own driving model is nothing entirely sophisticated, there are a few things I can get away with. I am also mostly using FOSS (free and open-source software) in Blender and Godot Game Engine.

I wish you well coming up with your simulator!
 
@JohnBM01 -- Thanks! :) You're right about Godot with its VehicleBody node. I tried it and wasn't pleased with it, which is why I started from scratch. It's been really surprising how useful the SpringArm node is. Last week I reused the suspension code for an experiment on a different kind of game, with impressive results.

FOSS is great. 👍 When I had a Macbook for a few years, the relative absence of FOSS and price tags on even the simplest "freeware-esque" software was a culture shock for me.
 
@r_outsider -- Wow, thanks! That's really cool of you. :cheers:

I've had one person PM me about the next entry, and I'm sorry it has taken so long. So without further ado:

V. Start Your Engines

For an engine simulation, we need a torque curve. I am using a common method as suggested by Marco Monster (Car Physics for Games) -- I keep a series of torque values (250RPM per step) in an array, and pull the torque value for the current RPM by interpolating between the two nearest entries in the array. The function to get the current torque value looks like this:
Code:
func torque():
    rpm = clamp(rpm, 0, rpm_limit)  # avoid array index error
    return lerp(torque_curve[ceil(rpm / 250) - 1], torque_curve[ceil(rpm / 250)], fmod(rpm, 250) / 250)
Now we can calculate the current torque value to apply to the wheels. Here's where it gets tricky. We have a torque value based on RPM that spins the wheels, but the RPM also depends upon how quickly the drivewheels are spinning. We need to allow the car to idle, a way to ease into 1st gear (ie. engaging a clutch), and a way of updating the RPM and drivewheel speed in tandem.

I found this tutorial for a drift simulator in Unity to be most helpful in getting over this hurdle, written by NoCakeNoCode. If you're using Unity, you'll probably find it even more helpful than I did. 👍

We have several variables to add:
engine_moment - Moment of inertia of the engine internals (eg. 0.25)
drive_inertia - engine_moment multiplied by the current gearing
engine_brake - Base value for engine braking/drag (eg. 10.0)
engine_drag - Drag that increases linearly with RPM (eg. 0.03)
torque_out - Stores the value we get from calling the torque() function above
drag_torque - Influence of engine_brake and engine_drag
rpm_limit - Also found above in the torque curve loop

My engine operation loop goes like this. First, I calculate drag_torque and grab the current torque_out. But there's something you might not expect:
Code:
drag_torque = engine_brake + rpm * engine_drag
torque_out = (torque() + drag_torque) * throttle
Why did NoCakeNoCode add drag_torque? To be perfectly honest, I can't think of a good physical explanation, but I can't argue with the results.

Now we apply the torque to the engine itself. I keep a constant called AV_2_RPM (= 60 / TAU, or 60 / (2 * PI)) to convert easily between angular velocity and RPM:
Code:
rpm += AV_2_RPM * delta * (torque_out - drag_torque) / engine_moment
if rpm >= rpm_limit:
    torq_out = 0  # Hit the limiter
    rpm -= 500  # You can make this a per-car variable if you like

Following NoCakeNoCode's example, the next part forks depending on whether the drivetrain is engaged or not. If the transmission is in neutral (or the clutch is depressed, if you're using a clutch input), simply rev the engine. Otherwise, apply the torque through the drivetrain. I use a method dedicated to each state.

In each case, the thing to do is get the average angular velocity of the drivewheels (reminder: I call it "spin" for brevity). That goes into determining both the speedometer readout (in meters per second, to be converted at the tail end to km/h or MPH) and the engine RPM for the next frame. Now is also the time to send torque to the SpringArm node (apply_torque()), which connects back to part IV above. The apply_torque() call sends the net driving torque, drive_inertia (which can be set after each gear change), and braking force (which I distribute in an array).

For simplicity, I will keep the examples to just RWD. Freewheeling is easy. We've already revved the engine, so just roll on and update the speedometer:
Code:
func freewheel(delta):
   var avg_spin = 0
   for w in range(4):
       # On the SpringArm's end: apply_torque(drive, drive_inertia, brake_torque, delta)
       wheel[w].apply_torque(0.0, 0.0, brake_torque[w], delta)
 
   for w in range(2,4):  # RWD
       avg_spin += wheel[w].spin * 0.5
   speedo = avg_spin * wheel[2].radius  # wheel 2 is rear left
When the drivetrain is engaged, calculate the net driving torque by multiplying the output and drag by the gear ratio. You will want to eliminate the drag once the drivewheels have come to a stop, or else drag will accelerate the car backwards. :P To do so, I just add back the drag_torque multiplied by the gear ratio.

There's still another step after this, because we need to split the torque at the differential in the rwd() method. However, you will see that we update the RPM by reading back the average spin, multiplied by the gearing and AV_2_RPM. But we're not done with that either:
Code:
func engage(delta):
   var avg_spin = 0.0
   var net_drive = (torque_out - drag_torque) * gear_ratio()

   for w in range(2,4):  # RWD
       avg_spin += wheel[w].spin * 0.5
   if avg_spin * sign(gear_ratio()) < 0:
       net_drive += drag_torque * gear_ratio()

   rwd(net_drive, delta)
   speedo = avg_spin * wheel[2].radius

   for w in range(2):  # Just brakes for front wheels
       wheel[w].apply_torque(0.0, 0.0, brake_torque[w], delta)

   rpm = avg_spin * gear_ratio() * AV_2_RPM
Alright, now here is how I do a differential; when I call apply_torque() on the SpringArm, I have it return a ratio of the result versus the projected result if the wheel was free-spinning. In the main script, I sum up the two results from each side in a way that lets me determine which wheel is the "winner" of that frame. For an open differential, as you might have guessed already, the "winner" gets a larger share of the torque next time. My limited-slip model is very basic as of yet; I just fix the torque split to 50/50 when applicable.

On the powertrain side, it goes like this:
Code:
func rwd(drive, delta):
    # if r_diff == 0 use r_split as is; open diff
    if r_diff == 1 and drive * sign(gear_ratio()) > 0:
        r_split = 0.5  # Simple 1-way LSD
    if r_diff == 2:
        r_split = 0.5  # Simple 2-way LSD
 
    var diff_sum = 0
    diff_sum -= wheel[2].apply_torque(drive * (1 - r_split), drive_inertia, brake_torque[2], delta)
    diff_sum += wheel[3].apply_torque(drive * r_split, drive_inertia, brake_torque[3], delta)
 
    r_split = 0.5 * (clamp(diff_sum, -1, 1) + 1)
Coming back to the SpringArm node, applying torque goes like this, incorporating part IV, and adding drive_inertia to the equation from before. You can also calculate rolling resistance and apply it here; just add it to brake_torque before you multiply it by sign(spin):
Code:
func apply_torque(drive, drive_inertia, brake_torque, delta):
    var prev_spin = spin
    # Initialize net_torque with previous frame's friction
    var net_torque = z_force * radius
    # Apply drive torque
    net_torque += drive
    # Stop wheel if brakes overwhelm other forces
    if abs(spin) < 5 and brake_torque > abs(net_torque):  spin = 0
    else:
        net_torque -= brake_torque * sign(spin)
        spin += delta * net_torque / (wheel_moment + drive_inertia)
 
    # Return result for differential simulation
    if drive * delta == 0:  # Don't divide by zero
         return 0.5
    else:
        return (spin - prev_spin) * (wheel_moment + drive_inertia) / (drive * delta)
Finally, we can spin the wheels and get our RPM...but what happened to idling or easing into 1st gear? That's one of the insights I got from the Unity tutorial -- the clutch can be done after everything else in a frame, no matter how advanced or simple it is. NoCakeNoCode also gives an example of a cleverly simple way to do a clutch. In his own words, "just pretend we are at a higher rpm."

We only need two more variables for that. A minimum engine RPM (ie. idle or stall RPM), and an RPM for pseudo-slipping the clutch, which can be a fixed variable, or different for each engine.
Code:
var clutch_rpm = rpm_idle
if gear == 1:
    clutch_rpm += throttle * clutch_out_rpm
rpm = max(rpm, clutch_rpm)
I love how simple it is. Suppose rpm_idle is 750 and clutch_out_rpm is 2500. When in first gear at low speeds, pressing the throttle will bring the revs up to a limit of 3250. The car will then accelerate using the torque available in that range, holding whatever RPM it has until the RPM exceeds 3250, after which the drivetrain is engaged like normal -- creating an effect that sounds and appears like slipping the clutch. In the same line, the RPM is held at the idle speed if the car is stopped.

I'm pretty sure there are games that have done that for a clutch and fooled me into thinking they had more of a clutch simulation. :)

EDIT 8/17/21: Fixed a typo in apply_torque() that omitted the drive torque, and changed a line in determining the differential split.
1/11/22: Added a missing abs() to brake lock condition
 
Last edited:
This week I have been working on the CPU drivers. I fixed a few bugs for them, and I hope to make them a bit more competitive in general for the next upload.

Meanwhile, fuel consumption is in place. I based it on brake-specific fuel consumption. Different vehicles can have different BSFC values, making them more or less efficient, and consumption is determined by engine output. The engine sputters and stalls once it's running on fumes (the last liter, or maybe it should be a smaller amount), and the quantity of petrol adds an accurate bit of mass.

There is no point in fuel consumption for sprints around the test circuit, of course, but it's preparation for the future. :)
 
Nice one! I'm happy to report that your driving simulator has passed the J-turn test, so great job with the tyre and suspension simulation :)

The loop is hard to do, it think because the circular shape absorbs too much energy from the start. I think a teardrop shape loop would be easier.

SidewinderSFEG.jpg
 
Nice one! I'm happy to report that your driving simulator has passed the J-turn test, so great job with the tyre and suspension simulation :)
Thanks! It has been difficult to bite my tongue and let you guys examine various maneuvers and details for yourselves. :lol:

The loop is hard to do, it think because the circular shape absorbs too much energy from the start. I think a teardrop shape loop would be easier.
I am familiar with that, but I think it also has to do with the bottom front edge of the car making contact on the way up. The Urban has the hardest time with it (even above the recommended speed I put up), while it's relatively easy for the Vuono. Or maybe that's the difference in torque. Or both.

I'm pleased with how the LFS-style cockpit cam conveys the chin-in-your-lap force of it. The default settings for that are in the middle, so it can be stronger, or turned off.
 
VI. Brushing Up and Misc. Details

This is the last main entry for "workshop" posts for now, sharing the formula for the "Brush" tire model, and some extras.

In part III, I provided the equation and some information for "Pacejka's Magic Tire Formula", or just Pacejka for short. It is a common model used in the tire manufacturing and motorsports industries, and capable of great sophistication, but it is an unwieldy model that requires many parameters based on curve-fitting with empirical data (for any one given tire compound) to make the most of it. Personally, I find most (consumer videogame) sims that are documented as using a Pacejka tire model to be lacking when the tires are pushed to their limits. I once considered that to be a fault of the model, but now I understand it probably has more to do with a lack of data.

Eventually, I uncovered the math for an alternative -- the "Brush" tire formula. The Brush model is another common and respected model, dating back to tire research in the mid-20th-Century. It conceptualizes a tire as a round brush with a series of bristles. When subjected to slip, the tire's "bristles" deflect; in short, the behavior of the tire is derived from a mathematical representation of these bristles and their stiffness. Unlike Pacejka's data-based parameters, the Brush model is a theoretical model that can simulate a range of states and tire compounds with fewer parameters. It is not cutting-edge, but it is effective.

Credit goes to this example written for MATLAB software, which demonstrates the structure of implementing the Brush formula in programming. I have rewritten and personalized my own form of the model.

The Brush tire model requires just a few variables:
mu - Coefficient of friction (example: 1.0)
con_patch - Length of the contact patch (example: around 0.15 to 0.25 meters)
tire_stiffness - Affects the tire's behavior in maintaining grip and how suddenly it begins slipping. Low values result in more of a "greasy" behavior, suitable for low-traction surfaces (example: 5 to 10)

Like for Pacejka, we need the current slip angle and slip ratio as explained in part III. There is also a final "stiffness" value for the model, combining the tire compound's stiffness value and contact_patch area, and the generic equation for friction, using our suspension y_force value:
Code:
    # 500000 because this line is multiplied by 0.5, while stiffness values are actually in the millions
    # tire_stiffness is a small value just for convenience
    var stiffness = 500000 * tire_stiffness * pow(con_patch, 2)
 
    var friction = mu * y_force
Now for the formula:
Code:
    # "Brush" tire formula
    var deflect = sqrt(pow(stiffness * slip.y, 2) + pow(stiffness * tan(slip.x), 2))
 
    if deflect == 0:  return Vector2.ZERO
    else:
        var vector = Vector2.ZERO
        var crit_length = friction * (1 - slip.y) * con_patch / (2 * deflect)
        if crit_length >= con_patch:
            vector.y = stiffness * -slip.y / (1 - slip.y)
            vector.x = stiffness * tan(slip.x) / (1 - slip.y)
        else:
            var brushy = (1 - friction * (1 - slip.y) / (4 * deflect)) / deflect
            vector.y = friction * stiffness * -slip.y * brushy
            vector.x = friction * stiffness * tan(slip.x) * brushy
        return vector
The chunk I've named "deflect" is akin to the hypotenuse of a triangle between the longitudinal slip value and the tangent of the lateral slip value, both multiplied by the brush's stiffness. If there is no deflection, there is no force to exert. If there is some deflection, the formula goes into one of two branches based on how much the tire has deflected. The variable named "brushy" is just another chunk of the formula, for convenience.

That's all there is to it, or at least as much as I have the knowledge to explain. So I am going to include a few extras/revisions:

Reading what kind of surface the tire is on
My method so far is to split surfaces into separate StaticBody nodes, and to assign those surfaces into groups ("tarmac", "grass", etc.) that are read by the RayCast, like this:
Code:
    if $Ray.is_colliding():
        if $Ray.get_collider().get_groups().size() > 0:
            surface = $Ray.get_collider().get_groups()[0]

Multiply by spring_length or not?
Currently, I have decided not to multiply y_force by the length of the spring. This is not how springs work, but it means you can adjust the ride height of a vehicle without having to adjust the spring stiffness to match, which seems like a worthwhile convenience to me.

I believe the damping (bump/rebound) calculation should also have included the spring_length in the first place, if you are interested in using real-world bump/rebound values (spring_length has no other effect).

Anti-roll/sway/stabilizer bar
This requires a couple parts, but if you have the SpringArm nodes return their "compress" value after calculating suspension forces, you can use an array to collect and distribute the compressions of each corner to calculate the force of an anti-roll/sway/stabilizer bar, with a stiffness value (anti_roll) and the compression value from the opposite side (opp_comp):
Code:
    ## Main script, attached to the RigidBody ##
    # Update wheels, distributing compression lengths to apply anti-roll bar
    # Use last frame's values to update opposite compressions in sync
    var prev_comp = susp_comp
 
    susp_comp[0] = wheel[0].update_forces(prev_comp[1], delta)
    susp_comp[1] = wheel[1].update_forces(prev_comp[0], delta)
    susp_comp[2] = wheel[2].update_forces(prev_comp[3], delta)
    susp_comp[3] = wheel[3].update_forces(prev_comp[2], delta)

-------------------------------------------

    ## SpringArm script ##
    if compress > 0:
        y_force += anti_roll * (compress - opp_comp)

Fuel consumption with BSFC
Lastly for now, my new fuel consumption method. This one is actually quite simple once figured out:
Code:
const PETROL_KG_L = 0.7489  # kg per liter of petrol
const NM_2_KW = 9549  # power = torque * rpm / [const]

func consume_fuel(delta):  # Brake Specific Fuel Consumption; kg per kilowatt-hour
   # 2nd gen Prius = 0.225 BSFC
   # thirsty turbo = up to 0.320 BSFC
   var fuel_burned = engine_bsfc * torque_out * rpm * delta / (3600 * PETROL_KG_L * NM_2_KW)
   fuel -= fuel_burned

-------------------------------------------------------------

I hope this will serve as a handy resource for anyone looking to develop their own simulation. :) As I said before, I have tried to keep the number of details relatively lean to make the guides easier to follow, piece by piece, but this is the whole essential principle of what I have built, and simulators like it.


EDIT: Previously this post had a small error in the brush formula. It did not amount to a whole lot in terms of behavior, but I have fixed it and changed this post.
 
Last edited:
GDSim is now updated to version 0.3 alpha, incorporating a big list of changes, including a new car to fill the mid-/rear-engined niche and a simple but unique new map inspired by the great Enthusia. The handling and CPU drivers have been much improved, and with that, I have publicly listed the project on itch.io (no longer a private link). It's officially live!

inside_evening.png


For convenience, here is the list of changes from the itch.io devlog:
NEW
  • New car - Caper DSF 2.0GS
    • Convertible sports car
    • Rear-engined, rear wheel drive
    • 130bhp boxer-4 engine
    • 900kg / 1984lbs. curb weight
  • New map - Meerbrücke
    • Endless straight bridge for testing
    • Supports CPU opponents and two-player racing
    • CPUs will drive until they run out of fuel
    • Inspired by Enthusia Professional Racing
  • New weather options - Rain and Wet
    • Extra challenge for humans and CPUs alike
    • "Rain" showcases an experimental method for beads of rainwater
    • "Wet" offers a partly cloudy sky with sunshine/stars
  • New grid options
    • "Solo" has been renamed to "Practice"
      • Lap entry now grays out to convey there is no lap limit
    • "Quartet" replaced by "Three Pairs"; now featuring six cars
    • "VS Random" - six cars, all CPUs randomized (including P1/P2)
    • Grid order is now reversed; conventional last-place start for P1
  • New (placeholder) gauge cluster for HUD
    • Coolant temperature gauge added (not just for show; see below)
    • Speedometer dial added
    • Gauges are now subject to virtual ambient lighting and illumination
    • Gauge background color and light color vary by vehicle make
  • New (placeholder) 3D gauges added for interior view
    • These gauges also illuminate, helping complete the night driving experience
    • Pressing Ctrl-H now cycles through full HUD, no HUD, and only the HUD gauges
    • Ctrl-H is also useful for the next new feature...
  • New redesigned "Screenshot Mode"
    • Press the camera button while the game is paused, or cycle cameras while following a CPU
      • An on-screen icon indicates activation
    • Standard first-person flying controls
      • "Engage 4WD" and "Clutch" control up/down (front shoulder buttons by default)
    • There are now just three standard camera views
  • Digital throttle/brake inputs are now damped (analog input unaffected)
  • Input device(s) may now be selected for "Reset to Defaults" buttons
    • Keyboard users can now easily set P1 to the default KB controls
    • P2 can be easily set to the default gamepad controls
  • "Window Up/Down" input now toggles convertible roof (Caper DSF)
    • Windows are now animated
    • Input renamed to "Windows/Roof"
    • Impact on sound effects remains the same as before
  • Added tire marks from wheelspin/lockup
  • Added tire smoke and dirt debris particles
  • Added antennas to vehicles
  • Added trees to Test Circuit

FIXES
  • "Reset to Defaults" buttons for P1/P2 controls did not work as intended
  • Race timer now begins at once for all cars on the grid, except in Practice
  • Rear longitudinal (Z) force cones displayed with "Show Forces" command now remain anchored in their correct positions
  • Corrected errors in celestial simulation
    • Solar declination was incorrect for the southern hemisphere
    • The moon's "dark" side could be visible previously because it did not rotate correctly
    • Sky gradient did not always display the intended colors/brightness
  • It is now possible to pause or return to the menu during a countdown

IMPROVEMENTS / ADJUSTMENTS
  • Improvements to CPU driver logic
  • New path for CPUs on Test Circuit, redrawn from scratch
  • Engine simulation now accounts for coolant temperature
    • Overheating will limit engine performance
  • Manual clutch control rewritten and greatly improved
    • Engagement more closely resembles the real thing
    • Now genuinely responsive to analog control
    • More effective digital control
  • Adjusted clutch torque effects
  • Handling refinements...
    • Anti-roll and damping/rebound settings are more relaxed
    • Maximum braking forces have been decreased
    • Tire pressure settings adjusted for each vehicle
    • Blackjack now has 1-way limited slip differential
  • Dynamic contact patch size increased for all tire compounds
    • Improved handling response and feedback
    • More appropriately balanced handling dynamics
  • Offroad surface values adjusted
  • Tire calculations now account for the effective radius when the tire's collision shape is "deformed" into the surface
  • Wheels can roll upon rocky terrain on Test Circuit again
    • Non-collision was only to help stray CPUs remain upright
  • Variable physics rate maintains higher values to improve physics stability
    • New "Very Low" setting updates at a fixed 120Hz for potato PCs
    • I test builds on my previous laptop, which is a Windows 7 potato with integrated graphics
    • I intend to add more performance-related options
  • Tire squeal is more audible from exterior views
  • Adjusted tire "stress" calculation
    • Tires should no longer chatter while rolling at slow speeds
  • Adjusted cockpit camera positioning to emphasize forward view
  • "Flora" graphic re-implemented in MultiMesh form; now 512 by default
    • Open to feedback; are flowers/etc. nice, or should I add tall grass?
  • Adjusted headlight and taillight ranges and angles
  • All vehicle meshes have been touched up, fixing some small errors
  • Tweaked test menu (still a placeholder, not the final design)
  • Selecting "Windowed" resets window to its starting size (1280x720)
 
I'm definitely going to have to give this a look. Does it currently support wheels for input? Or if not, is that planned?
 
I'm definitely going to have to give this a look. Does it currently support wheels for input? Or if not, is that planned?
FFB is a challenge beyond my programming abilities, because Godot does not support FFB without extending its functionality. It also might not detect some button IDs, because the built-in input system expects a gamepad. However, you can configure the controls to pick up the wheel and pedal axes -- I have given that a go myself.

If I find a way to add FFB, I will give it a try! If you try analog stick steering, please experiment with the settings in the "Input" panel -- the default settings are very much open to change.
 
FFB is a challenge beyond my programming abilities, because Godot does not support FFB without extending its functionality. It also might not detect some button IDs, because the built-in input system expects a gamepad. However, you can configure the controls to pick up the wheel and pedal axes -- I have given that a go myself.

If I find a way to add FFB, I will give it a try! If you try analog stick steering, please experiment with the settings in the "Input" panel -- the default settings are very much open to change.
Even without FFB, it might still be worth controlling with direct steering inputs from a wheel. I figure it's gotta be better than a controller or arrow keys. In fact, with the right mapping tool, I might be able to get my wheel to work without any changes on your end, but that depends on me bothering to set it up... 😅

Some thoughts based on a few minutes of screwing around:

  • Interpreting input from an analog stick and mapping it to car steering is difficult, and every game does it differently. I was able to get something workable by messing with the settings you mentioned, but it's still not exactly easy. I definitely need to set deadzones, at least for the camera controls, as any little touch sends the camera to the side or rear which is...not conducive to driving.
  • The physics behave really well, and it's clear the low-speed physics and tire dynamics are robust. I did notice it was unexpectedly easy to end up climbing my wheels up one of the walls on the Test Circuit and riding it like a steeply banked corner, but maybe that's a side effect of the terrain allowing me to bounce into the air more, combined with the lack of damage.
  • On that note, even without any damage modelling, collisions felt surprisingly realistic. It felt like the momentum of each car carried more accurately than in other games. The ability to pit-maneuver (or be spun by) other vehicles is nice, and feels accurate from my inexperienced perspective.
  • The Test Circuit is difficult to contend with. I think the polygon count of the track surface itself can make driving on it more unstable and bumpy than is ideal. I'd like to try driving on a track that's wider, flatter... maybe even just a big open space for experimenting with the vehicle dynamics without many boundaries. Perhaps it could be called "Flughafenplatz" 😉
  • It seems drafting is implemented, which was a pleasant surprise! It makes racing opponents at Meerbrucke much more interesting, that's for sure.
  • The AI is far from perfect, but I'm impressed none-the-less. I'd be interested in a (very simplified) summation of how you went about it, as I'm sure there are many many different options.
Overall I'm quite impressed, and happy to throw a few bucks your way. I was just thinking the other day that, considering all the people on this forum with strong understandings of vehicle physics, there must be some capable of developing at least a basic physics model as a proof of concept, and this far surpasses what I was expecting.

So, props to you! Looking forward to seeing updates whenever they may come.
 
@dylansan -- Thanks for the detailed feedback, and especially for the purchase!
Interpreting input from an analog stick and mapping it to car steering is difficult, and every game does it differently. I was able to get something workable by messing with the settings you mentioned, but it's still not exactly easy. I definitely need to set deadzones, at least for the camera controls, as any little touch sends the camera to the side or rear which is...not conducive to driving.
It has already occurred to me that I really ought to have added speed sensitivity (at least as a choice) to the steering by now. You can expect it very soon; I already have an implementation in progress.

Deadzone configuration is another thing I ought to have. The menu needs an overhaul, but until I undertake that surgery, I can enlarge the camera control deadzones, because the only reason to have smaller deadzones is for the "screenshot mode" camera. For reference, what kind of controller are you using?
The physics behave really well, and it's clear the low-speed physics and tire dynamics are robust. I did notice it was unexpectedly easy to end up climbing my wheels up one of the walls on the Test Circuit and riding it like a steeply banked corner, but maybe that's a side effect of the terrain allowing me to bounce into the air more, combined with the lack of damage.
That means a lot coming from a fellow Enthusia fan! The wheels have spheres for colliders, so there is a degree of protrusion out the sides. They only collide with designated surfaces, so door-to-door contact is unaffected, but there is little I can do to prevent them from climbing steep ground-surface-type slopes without trading for a collision shape that is less optimal overall.
On that note, even without any damage modelling, collisions felt surprisingly realistic. It felt like the momentum of each car carried more accurately than in other games. The ability to pit-maneuver (or be spun by) other vehicles is nice, and feels accurate from my inexperienced perspective.
Masses are accurately scaled, and for a bit of trivia, the underlying physics engine is the same one used in Grand Theft Auto IV. I share your observation that PIT maneuvers seem neither too easy nor too difficult.

It might be related to my personal solution for manipulating the polar moment of the vehicles? For another bit of trivia, the vehicles are literally skewered by an invisible cylinder I use to adjust the polar moment of inertia (willingness to rotate). This solution also improves the distribution of mass, because the engine assumes a physics body has uniform density within its shape.
The Test Circuit is difficult to contend with. I think the polygon count of the track surface itself can make driving on it more unstable and bumpy than is ideal. I'd like to try driving on a track that's wider, flatter... maybe even just a big open space for experimenting with the vehicle dynamics without many boundaries. Perhaps it could be called "Flughafenplatz" 😉
Detail-wise, the thing is that Test Circuit is literally the first map I made that was at all worth keeping! I have refined my map-making technique since then, and I may divide polygons in spots that are more TDU1-like. Design-wise, it is deliberately narrow and dynamic for evaluating the handling and the CPU logic. The downhill braking zone into the hairpin has been a productive challenge for the CPUs.

I have a second circuit in the works that is wider and more racetrack-like. As for a larger open tarmac area, I may go ahead with an idea I've had that is a bit more interesting than Airport Square. 🐺

Going into this as a total novice at 3D modelling, after an early learning curve, it is really surprising how simple it is to work in 3D when you keep the polygon count low and flip the bird at Blender in favor of a less-sophisticated alternative (I prefer Wings3D).
It seems drafting is implemented, which was a pleasant surprise! It makes racing opponents at Meerbrucke much more interesting, that's for sure.
It is! That's one detail I've been waiting for someone to notice. 👍

The AI is far from perfect, but I'm impressed none-the-less. I'd be interested in a (very simplified) summation of how you went about it, as I'm sure there are many many different options.
I'd like to highlight that with the programming background I have, it is shocking that they can do what they do. My solution is a homegrown invention, and I don't know how relevant it is to common solutions.

The gist of it goes like this:
  1. I draw a path along the circuit, and this path is followed by two helper objects for each CPU -- a "rabbit" and a "hound"
  2. The hound follows the path at a relatively fixed distance ahead of the CPU, while the rabbit leads the way relative to the hound's position
  3. The faster the CPU is driving, the further ahead the rabbit travels; this represents looking ahead
  4. The CPU logic compares the vector of the hound and rabbit, so when the rabbit reaches a curve or kink in the path, it registers a delta in the path vector
  5. This delta determines throttle use, and when it exceeds a threshold, it instructs the CPU to hit the brakes
  6. Braking decreases the leading distance for the rabbit, pulling it back from the curve/kink; this creates a loop that causes the CPU to brake until it has reached the corner (because the hound has caught up to the rabbit)
  7. Meanwhile, the CPU compares its heading to the vector pointing to the hound; this is the main factor for steering
  8. The steering is also influenced by the vector pointing from the hound to the rabbit -- again, to keep the CPU "looking" further ahead
  9. If wheelspin is detected, or oversteer has rotated the CPU away from pointing to the hound, the CPU backs off the throttle
  10. When a particularly sharp corner is detected, the CPU stays on the brakes and might even tug the handbrake (under acceptable conditions) until it is pointing in the right direction again
 
Last edited:
FFB is a challenge beyond my programming abilities, because Godot does not support FFB without extending its functionality. It also might not detect some button IDs, because the built-in input system expects a gamepad. However, you can configure the controls to pick up the wheel and pedal axes -- I have given that a go myself.
I've not read everything you posted, nor tested the last version (will do both later, with more time), but about this specific topic, have you seen this and this?
 
Last edited:
I've not read everything you posted, nor tested the last version (will do both later, with more time), but about this specific topic, have you seen this and this?
I have seen it. Supporting input from the controller is one thing. The question is whether the Godot devs will see fit to incorporate the ability to send FFB signals to wheels? That could be something that is too niche, particularly within the Godot community at present, for the devs to want to include it as core functionality.
 
It's no Flughafenplatz, but version 0.4a introduces a new map with a feature I'm proud to introduce -- autocross, with support for creating and sharing your own layouts! Or gymkhana, or street-style course, or a drifting park; whatever you like.



Importantly, I have also implemented a save file for your settings, including controls and deadzones. Next, I have added functionality for custom setups; wheel alignments and gearing are factory settings to the best of my knowledge, but I made up the rest and I'm certain you guys can come up with better. This also means you can swap tires, change differentials, and drain some fuel weight.

There is also now a MacOS version available. 👍

Then there's the Inari CA2 GT-S; I was considering supercharging it to match it up to the others, but it seems to do well with just the naturally-aspirated you-know-what, making it the best car for showing how the simulation behaves without quite so much torque or wheelspin. Another powertrain layout crossed off the list. Next, a Dymaxion lookalike! Just kidding. 🐺
 
Thanks for the write-up/guide. I've been following along and implementing parts of your approach in an attempt to improve my own work on a raycast vehicle in Godot (old video here Godot Raycast Car). However my implementation has a few issues and I wondered if you could clarify a couple of points from your code?

Firstly in section III you discuss angular velocity or 'spin', and I suspect that I am either not calculating this correctly, using the basic formula angular_velocity = forward_velocity / tyre_radius, or plugging in the wrong values, as I get odd behaviour such as my car going into reverse if it has freewheeled backwards or collided with a static body in the environment.

Secondly, I assume that the spin value generated from the apply_torque function should be used as the actual force value to drive the wheels, something like this car.apply_impulse(car.global_transform.basis.xform(car.to_local(ray.get_collision_point())), global_transform.basis.y * spin

Apologies if these questions seem obvious or stupid.
 
Thanks for the write-up/guide. I've been following along and implementing parts of your approach in an attempt to improve my own work on a raycast vehicle in Godot (old video here Godot Raycast Car). However my implementation has a few issues and I wondered if you could clarify a couple of points from your code?

Firstly in section III you discuss angular velocity or 'spin', and I suspect that I am either not calculating this correctly, using the basic formula angular_velocity = forward_velocity / tyre_radius, or plugging in the wrong values, as I get odd behaviour such as my car going into reverse if it has freewheeled backwards or collided with a static body in the environment.

Secondly, I assume that the spin value generated from the apply_torque function should be used as the actual force value to drive the wheels, something like this car.apply_impulse(car.global_transform.basis.xform(car.to_local(ray.get_collision_point())), global_transform.basis.y * spin

Apologies if these questions seem obvious or stupid.
About the second question, the force from the wheels would be the frictional force, which isn’t proportional to the amount of wheelspin. As long as there is wheelspin, the force is equal to the maximum frictional force of the tyre, and when there is no wheelspin the force is equal to the wheel torque/radius.
 
Last edited:
Firstly in section III you discuss angular velocity or 'spin', and I suspect that I am either not calculating this correctly, using the basic formula angular_velocity = forward_velocity / tyre_radius, or plugging in the wrong values, as I get odd behaviour such as my car going into reverse if it has freewheeled backwards or collided with a static body in the environment.

Secondly, I assume that the spin value generated from the apply_torque function should be used as the actual force value to drive the wheels, something like this car.apply_impulse(car.global_transform.basis.xform(car.to_local(ray.get_collision_point())), global_transform.basis.y * spin

Apologies if these questions seem obvious or stupid.
No worries. 🐺 I will add an edit to help clarify this part.

What we're doing here is actually treating a wheel's angular velocity more like an independent physics property, acted upon by the sum of torques on each wheel as shown in section IV. The wheels have their own virtual mass and momentum together with that angular velocity, or spin.

The drive force at the wheels is a product of the tire formula (whether it's the Pacejka formula, Brush formula, or another formula), which involves a comparison between spin * radius and the Z velocity (in a localized reference frame) to calculate the slip ratio, (spin * radius - z_vel) / abs(z_vel). So spin has a naturally indirect effect on the force value moving the car with the driven wheels. From Newton's third law, the same tire force that moves the car also exerts a torque back on the wheel, which is used to initialize the torque sum each frame.

In other words, there are distinct relationships of powertrain <-> wheel <-> tire <-> surface.
 
Last edited:
Back