I make stuff

screenshot
Next article ►

AV-Racer Devlog (1): Getting a functional car model

est. reading time: 10 minutes

The first question revolving around the interest of making a car game was figuring out the methods used by car games in simulating a car body. My modest lacunary conglamurate of car facts was all I had to start with, the general idea was to simulate car and tire forces that would move the thing through the might of physics.

The right approach to this problem became a matter of discussion and debate between me and my close friend Said Al Attrach, we came across a very interesting website that provided useful information to start our project with.

I setup a very basic project pipeline, using SDL2 to handle windows, events and rendering (later switching to my own renderer with OpenGL), made a quick couple of drawings on Aseprite and rendered a car in the center of the screen, a generic map behind it, and moved the car in a virtual sim space, moving the camera to follow its position as the car moves Camera.P = Car.P; I set up ImGui in the game to streamline any changes or input we needed to use, it was and still is a great decision, very useful for debug.

The car had a 2D space position P and velocity V, we used input from the keyboard to add a constant force to a local acceleration value that we added to the car's velocity through the loops, the velocity adds to the position. We also had an angular velocity aV that adds to the car's Angle which rotates the sprite.

We didn't have a good understanding of car physics and our attempts at a somewhat realistic simulation failed, so we resorted to an arcade-like model, which I will describe in the next section.

Settling for a simpler model

I started from scratch. By that time the car already had an RPM system and a dysfunctional traction calculation, I started by simplifying the car into a single object, not four tires. One tire, or closer to a rolling ball.

To move the car we need to add some sort of force proportionate to “Gas” input to the total velocity of the car over time, i.e. acceleration. We need a 2D vector that adds to the 2D vector V to modify it. The acceleration vector should always point in the direction the car's pointing in, that's the direction the accelerating force would be applied.

We therefore need the normalized car's longitudinal vector which we can derive from the car's angle (Angle), we can also get the lateral vector from it which we would need later. Then all we need to do to construct the acceleration vector is to multiply the vector with a modifier that is dependent on the gas input (Gas), a float from 0.0 to 1.0 that represents how much the gas was pressed, then multiply all of that with a coefficient we eyeball through testing to adjust how intensive the throttle input is:


    v2 Lon = v2(cos(Angle)), sin(Angle));
    v2 Lat = v2(-Lon.y, Lon.x);
    v2 Acceleration = Lon * Gas * TorqueCoeff;
      
Next we need can construct a vector that represents brake power. When hitting the brakes, the forces acting on the car causing it to slow down can be represented in a single simple force that acts in the opposite direction of the car's velocity vector; however way the car is headed, the braking force acts in the opposite direction. This force would be proportionate to the input of the brake pedal (0.0 to 1.0) and is scaled by a brake coefficient.
Note how we need to normalize the velocity vector to use it as a direction.

    v2 Braking = -Brake * BrakeCoeff * Car.V.normalize();
            
Putting these together the car might oscillate when braking as it overshoots at low speeds. There are two ways to fix the problem and I implement both: interpolation, and simulating friction forces.

Friction forces are the forces that bring an object to a halt when there's no other forces applied to it, in a sound and accurate physical simulation this force is relevant in its role in slowing the car down. At high speeds the air drag resistance plays a larger role at slowing the car down. In our case I use friction forces at low speeds where they're mostly relevant. Remember the goal here was to make a car that feels good to drive and not necessarily a simulation.

It is relevant at this point to introduce a nifty little mathematical operation I use it all over the place in this project. A logarithmic scaler:

    float logarithmic_incr(float input, float k){
        return (exp(input * k) - 1) / (exp(k) - 1);
    }
    float logarithmic_decr(float input, float k){
        return -(exp(input * k) - 1) / (exp(k) - 1) + 1;
    }
            
It uses a simple exponential equation and a modifier to adjust a curve that always crosses 0 and 1 and therefore can be used to modify values on a logarithmic scale from 0 to 1. Try it here.

I use the logarithmic scaler with the length of the velocity vector as a modifier to the friction forces to only be relevant at low speeds, and multiply it with a negative coefficient to act -like braking- in a direction opposing the car's. We construct the following vector:


    v2 Friction = -FrictionCoef * Car.V.normalize() * logarithmic_decr(Car.V.length(), -500);
            
The fourth main force relevant to the car is longitudinal resistance, a force that represents air drag, friction, or other forces that slow the car down at high speeds. This force is proportionate to the car's speed, the faster the car is, the higher it will be. This force limits the car's indefinite acceleration. We can therefore multiply it by the velocity vector directly and a negative coefficient.

    v2 Resistence = -DragCoeff * Car.V;
        
All of these coefficients present a tall order of calibration, but they mostly depend on the sim's units, car's relative dimensions and how one would like to make the car feel, so there isn't really any clear cut “right” values for them. Their point is to make the job easier to calibrate the vectors, and offer an easy way to tweak car performance in gameplay.

Adding all those vectors to one we get the following, which we apply to the velocity vector:

    v2 VelocityChange = Acceleration + Resistence + Friction + Brake;
    Car.V += VelocityChange;  
        
Next we can look into the car's angular velocity and change of its angle. Here I hacked the method. The result I was looking for was a car that turns with a wider angle the faster it is, but in order to program a proper angular velocity I would have to deal with lateral forces that stop the car's rotation. I ended up just clamping the angular velocity between two limits that grow or shrink based inversely proportional to the car's speed. I created a float angular_v_limit and it was calculated from the exponent of the negative car speed and clamped between 0 and a value that was calibrated through testing to what looked like a good maximal rotation speed. The result made the car's angular velocity effectively less intense the faster the car was.

Then the change to the car's angular velocity would be proportionate to how far the wheels were turned, a coefficient to easily adjust the values, and then clamped to be within the angular limit defined above.

    float AngularVChange = SteerCoeff * WheeleAngle;
    Car.AngularV += AngularVChange;
    Car.AngularV = clamp(Car.AngularV, -angular_v_limit, angular_v_limit);
        
After some adjustment of the coefficients, the above results in a car that turns well and drives plausibly, but is infinitely grippy. Now, while I decided against any complex driving mechanics that can result in a loss of grip through loss of traction, slip, or car weight related systems, a “fake” sliding of the car as it turns can result in a fun looking and feeling car that is easy to drive.

To create a sliding effect, we can make the car face an angle different that it it is heading, this is, in essence, what drifting or sliding is. To achieve this, we can deconstruct the velocity vector of the car into a longitudinal and lateral components and scale down the lateral component by a certain amount while keeping all the calculations above as is. This deconstruction is done to the final car velocity vector after applying the forces and the resulting effects manifest organically.

To deconstruct the velocity vector we take the dot product of it and the longitudinal or lateral vector respectively and multiply by that vector. The dot product is like dropping a shadow of a vector onto another and measuring the length of that shadow, and the multiplication of said length with a normalized vector results in a vector pointing in that direction that has a length of the dot product.

After we deconstruct the vector we reconstruct it with the lateral vector scaled down using a slide coefficient between 0.0 and 1.0 we adjust through testing. All of this is applied after VelocityChange is added to the car's vector.


    v2 LonV = dot(Car.V, Lon) * Lon;
    v2 LatV = dot(Car.V, Lat) * Lat;
    Car.V = (LonV + LatV * SlideCoeff);

    
    Car.P += Car.V ;
    Car.Angle += Car.AngularV;
        

Substepping

Substepping is running the simulation in multiple smaller substeps per frame to increase simulation accuracy and prevent simulations from “blowing up”, which shortly put, is when a simulation runs very large value changes per step that result in inaccurate handling of the physics running behind. The solution to this is to make the simulation run smaller steps and this is a simple way to solve this.

The approach is simple, you loop over the same simulation an N number of substeps but also multiply every change you apply to the system by 1/N.

So let's say our simulation is a function that takes a float dt that is defined as: float dt = 1.0f / substeps; we can achieve accurate results be multiplying relevant changes to the car's angular and longitudinal velocities, angle and position with dt:

    Car.V += VelocityChange *dt;
    Car.AngularV += AngularVChange *dt;
    Car.P += Car.V *dt;
    Car.Angle += Car.AngularV *dt;
        
Putting all of this together, and calibrating the coefficients, we get the following result:

Car Physics result 1

This looks better and more consistent doesn't it? The car feels like it slides well but you're always in control, it fits an arcade racer. This system is used in the game in its current state. The different coefficients have been crucial in adjusting gameplay, ImGui's interfaced made those changes easy to apply.

The SlideCoeff later became a changing variable that varied depending on whether the car was on or off track, which added nuance and more options for varied gameplay. The value could also be used to simulate wet or snowy surfaces alongside the friction and resistance vectors.

The next problem

While this current result looks fairly decent and the car feels like it slides okay, it's not perfect. For instance, the car seems to be in constant sliding every time it turns, plus the rotate ratio around the car's axis doesn't match well with the turn angle nor what a car should look like turning. This all makes the car look and feel somewhat like a hovercraft and not a vehicle in contact with the ground. There is some imporvement to be made on this existing "fake it all" model without necessarily having to simulate anything. Thanks to Casey Muratori for his insight and feedback after playing this. I wouldn't have revisted it if it weren't for his feedback. So Said and I went back and spent an evening on it.

One very basic thing to scratch is the angular_v_limit which was already too duct-tape to be viable. The car had arbitrary limits to the angular velocity, which, while proportionate to the angular velocity, limited the physics of the whole car, impacting further things like collision resolving. The car wouldn't spin well.

We need to handle the angular inertia differently:

    float steer = Car.Angle * steer_coef;
    ...
    Car.AngularV += (steer)*dt;
    Car.AngularV -= n * Car.AngularV * dt;        
        
Here we have two variables that we need to define relative to the game world. The steer_coef is a scaler that is also proportionate to the car's velocity to limit turn angles at low and high speeds. The n is a float with a value [0,1] which works against the adding of angular velocity, dampening that effect. An angular decelerator, if you will. Both of these scalers need to be adjusted to the game world. Which requires some eyeballing.



The car is now a bit more realistic and looks less like a hovercraft. This preview looks more polished as these changes were done in the late stages of development.

The second part of making the car feel better at driving is how the car handles sliding. And I mean the physical and not the visual aspect. We could always render tire marks and smoke whenever, but how the car turns, slides, and grips is where this model needed improvement. At that point the car just immediately slides as soon as the the car starts turning, which is by design so far, as we deconstruct the velocity vector as explained above.

The desired behaviour is that the car would actually be grippy up to a certain point where it would lose that grip due to excessive cornering forces, akin to how real cars behave. How do we achieve that effect without necessarily simulating all these different tire forces? Enter the accumulator.

The accumulator is an arbitrary value that increases the harder the car is turning up to a limit and that value gets multiplied in the scaler that is used to scale down the lateral component of the velocity vector. This model allowed the freedom to decide the criteria in which the accumulator and scaler would increase and decrease to tune it such that the car feels good.
The accumulator increases the larger the lateral velocity is and cools down when there is no added velocity, clamped between two values adjusted through testing. The cooldown is also designed such as in the absence of a large lateral velocity, the accumulator would cool down faster. Which in practical terms means the car would recover from a slide quicker if there is no turning.


    slide_accumulator += LatV.length() * dt;
    slide_accumulator -= (1 - clamp(LatV.length() * a, 0, 1)) * cooldown * dt;
    slide_accumulator = clamp(slide_accumulator[p_i], 0, b);
    
cooldown is a float coefficient adjusted to the game world. a,b are scaler coefficients adjusted through testing. the former scales up or down the lateral velocity which affects the cooldown speed. The latter limits the accumulator to a value that works with the slide coefficent.


Observe the accumulator going up and down as the car turns. Capping at a value where the car has lost grip. The car otherwise grips relatively well, which is the desired effect.

In the end, the slide_coef is multiplied by the accumulator, car's velocity, brake strength, and handbrake. Those four variables increase and decrease the sliding to achieve a balanced feel to the car with minimal physical caluclation.

In the next article I will be talking about track creation and management and how the game handles all of that.
Thanks for reading, and thanks for playing!

-Wassim
Next article ►