Tutorials, Research and Thoughts

Create Your First TORCS Racing AI Bot - Part 03: Aerodynamics and Stability Controls

This is the fourth chapter of a tutorial for building an AI agent for the racing game TORCS. In this chapter, we will learn to break, change gears and drive faster.

Aerodynamics

So far, we omitted many variables to simplify computations. Now we will include an additional concept that plays a major role on our car performance: aerodynamics.

Aerodynamics can be defined as the study of the motion of air on a solid body. The figure below illustrate different aerodynamic forces applied on a car at speed: the drag, the lift and the downforce. There are also some other ones like the yaw moment (not represented here). We will take into account the downforce and the drag in our calculations.

Aerodynamic Forces

Going in depth in explanation of aerodynamic phenomenon might be a bit out of scope of this tutorial. This chapter is actually a bit lenghty right. But if you are interested, some good resources for our topic are:

We will revisit the speed limit in turns and the braking mechanism with aerodynamics.

Accessing the default car setup

Do you remember the directory where we listed the available cars? Well, within each car directory there are various files among which an xml one that defines our car's properties, including the setup. In our case, the xml properties file is at /usr/local/share/games/torcs/cars/car1-trb1/car1-trb1.xml. These setup are used by default for our AI agent car.

We will discuss more about managing car setup in coming chapters. For now, we need to get some setup from the file, relative to aerodynamics.

One function we will use to access numerical values in the default setup file is GfParmGetNum. The sections of interest in car1-trb1.xml is the following:

    <section name="Aerodynamics">
        <attnum name="Cx" min="0.20" max="2.0" val="0.35"/>
        <attnum name="front area" unit="m2" min="1.0" max="3.0" val="1.92"/>
        <attnum name="front Clift" min="0.0" max="1.5" val="0.69"/>
        <attnum name="rear Clift" min="0.0" max="1.5" val="0.7"/>
    </section>

    <section name="Front Wing">
        <attnum name="area" unit="m2" val="0.25"/>
        <attnum name="angle" unit="deg" min="0" max="12" val="6"/>
        <attnum name="xpos" unit="m" val="2.20"/>
        <attnum name="zpos" unit="m" val="0.04"/>
    </section>

    <section name="Rear Wing">
        <attnum name="area" unit="m2" min="0" max="1.0" val="0.7"/>
        <attnum name="angle" unit="deg" min="0" max="18" val="14"/>
        <attnum name="xpos" unit="m" min="-2.5" max="-1.0" val="-2.01"/>
        <attnum name="zpos" unit="m" min="0.1" max="1.5" val="0.99"/>
    </section>

Aerodynamics in the speed limitation

Let's include the downforce in the equation we previously used to find the speed limit in turns.

$$ \begin{align*} \underbrace{\frac{m \cdot v^2}{r}}_{\text{Centrifugal force}}& = \underbrace{m \cdot g \cdot \mu}_{\text{Friction force}} + \underbrace{C_a \cdot v^2 \cdot \mu}_{\text{Aerodynamic downforce}} \\ v^2& = \frac{g \cdot \mu \cdot r}{1 - r \cdot C_a \cdot \mu / m} \\ v& = \sqrt{\frac{g \cdot \mu \cdot r}{1 - min(1, \frac{r \cdot C_a \cdot \mu}{m})}} \end{align*} $$

Where \(C_a\) is the downforce coefficient. The downforce coefficient can be considered a fixed value computed using the density of the air, the area, shape and angle of attack of the car's wing and the ride height which is the distance between the ground and the car's bottom. Two things actually contribute to the downforce: the car wings which act like an inverted airplane wing to keep the car on the ground, and the ground effect which is cause by the flow of air between the car and the ground. The downforce is a performance advantage on cars with wings and a specially designed chasis.

When solving the equation for \(v\) there is a risk of getting a negative number under the square root (a complex number), in case \(r \cdot C_a \cdot \mu / m\) becomes greater than 1. To avoid that we use the minimum function to put a restriction.

Let's start the implementation with the method that initialize the downforce coefficient.

/**
 * Initialize the aerodynamic coefficient CA of the car.
 */
void CarController::InitCa()
{
    const float air_density = 1.23;
    float rear_wing_area = GfParmGetNum(car->_carHandle, SECT_REARWING, 
                    PRM_WINGAREA, (char*) NULL, 0.0);
    float rear_wing_angle = GfParmGetNum(car->_carHandle, SECT_REARWING, 
                    PRM_WINGANGLE, (char*) NULL, 0.0);
    float front_ground_effect = GfParmGetNum(car->_carHandle, SECT_AERODYNAMICS, 
                    PRM_FCL, (char*) NULL, 0.0);
    float rear_ground_effect = GfParmGetNum(car->_carHandle, SECT_AERODYNAMICS, 
                    PRM_RCL, (char*) NULL, 0.0);

    float front_right_height = GfParmGetNum(car->_carHandle, SECT_FRNTRGTWHEEL, 
                    PRM_RIDEHEIGHT, (char*) NULL, 0.20);
    float front_left_height = GfParmGetNum(car->_carHandle, SECT_FRNTLFTWHEEL, 
                    PRM_RIDEHEIGHT, (char*) NULL, 0.20);
    float rear_right_height = GfParmGetNum(car->_carHandle, SECT_REARRGTWHEEL, 
                    PRM_RIDEHEIGHT, (char*) NULL, 0.20);
    float rear_left_height = GfParmGetNum(car->_carHandle, SECT_REARLFTWHEEL, 
                    PRM_RIDEHEIGHT, (char*) NULL, 0.20);

    float wing_ca = air_density * rear_wing_area * sin(rear_wing_angle);
    float Cl = front_ground_effect + rear_ground_effect;

    float ride_height = front_right_height + front_left_height 
            + rear_right_height + rear_left_height;
    float h = 2.0 * exp( -3.0 * pow(ride_height * 1.5, 4));

    CA = h * Cl + 4.0 * wing_ca;

}

We can now change the last line of GetAllowedSpeed(tTrackSeg* segment) from

        return sqrt(mu * G * r);

to

        return sqrt((mu * G * r) / (1.0 - MIN(1.0, r * CA * mu / full_car_mass)));

In order to take into account the aerodynamic downforce when we compute the maximum allowed speed in turns. We modify the NewRace(...) method to initialize the mass of the car and the the downforce coefficient by calling InitCa().

void CarController::NewRace(tCarElt* car, tSituation* s)
{
    this->car = car;
    car_mass = GfParmGetNum(car->_carHandle, SECT_CAR, PRM_MASS, NULL, 1000.0);
    InitCa();
}

We also have to update the car mass. We do it in the CarController::Drive(...) function, just after the memset(...) line.

//...
    memset((void *)&car->ctrl, 0, sizeof(tCarCtrl)); // reset the values

    full_car_mass = car_mass + car->_fuel;
    float car_angle = CurrentCarAngle(situation);
//...

Now let's add the declaration of the new class members in carcontroller.h.

    private:
        //...
        void InitCa();

        //...

        float car_mass;
        float full_car_mass;
        float CA;

        //...

The graph below show a comparison of the maximum speed that can be reach in turns in relation to the radius of the turn, between the old and the new GetAllowedSpeed(tTrackSeg* segment) method.

Comparison

Test drive

lap time

Aerodynamics in braking

We will make use of the aerodynamic downforce and the drag in our calculations. This will allow us to break later and smoothen the braking pattern when driving.

All right, the next formula maybe intimidating but, take a deep breath and stay with me please... Let's rewrite the energy conservation equation we used to compute the braking distance, by including the downforce and the drag.

$$ \underbrace{\frac{m \cdot v_1^2}{2}}_{\text{Current kinetic energy}} - \underbrace{\frac{m \cdot v_2^2}{2}}_{\text{Desired kinetic energy}} = \underbrace{m \cdot g \cdot \mu \cdot s + C_a \cdot \int_{s_2}^{s_1} v^2(s) ds \cdot \mu }_{\text{Energy burned by brake}} + \underbrace{C_w \cdot \int_{s_1}^{s_2} v^2(s) ds}_{\text{air resistance energy (drag)}} $$

...Ah, that is enough math for today... So here is the braking distance we get when solving the equation.

$$ s = - \frac{ln((c + v_2^2 \cdot d) / (c + v_1^2 \cdot d))}{2 \cdot d} $$

With \(c = g \cdot \mu\) and \(d = (C_a \cdot \mu + C_w) / m\):

\(C_w\) is the drag coefficient. The drag coefficient is computed using ...

Here is the implementation of the initialization of \(C_w\)

/**
 * Initialize the aerodynamic drag coefficient Cw
 */
void CarController::InitCw()
{
    float Cx = GfParmGetNum(car->_carHandle, SECT_AERODYNAMICS, 
                    PRM_CX, (char*) NULL, 0.0);
    float front_area = GfParmGetNum(car->_carHandle, SECT_AERODYNAMICS, 
                    PRM_FRNTAREA, (char*) NULL, 0.0);
    CW = 0.645 * Cx * front_area;
}

Now we change the following line from GetBrake(...)

            float brake_distance = 
                    (pow(speed, 2) - pow(allowed_speed, 2)) / (2.0 * mu * G);

to

            float c = mu* G;
            float d = (CA*mu + CW) / car_mass;
            float v1sqr = pow(speed, 2);
            float v2sqr = pow(allowed_speed, 2);
            float brake_distance = 
                    - log((c + v2sqr * d) / (c + v1sqr * d)) / (2 * d); 

We declare the new class members in carcontroller.h

    private:
        //...
        void InitCw();

        //...
        float CW;

and we call InitCw(...) at the end of NewRace(...).

void CarController::NewRace(tCarElt* car, tSituation* s)
{
    //...
    InitCw();
}

Test drive

Let's try a test drive:

Lap time

Stability Controls

During the test drive you may have notice the car wheels slide at some points (leaving marks on the track). This is due to accelerating and braking too hard. We will use some stability control approaches to alleviate this phenomenon.

Stability controls are mechanisms that improve the stability of a car while driving. There are different stability control systems including the Antilock Brake System (ABS) and Traction Control (TCL) that we will implement for smooth acceleration and braking.

Antilock Brake System (ABS)

Suddenly applying full brake to a speeding car will lock the tyres, which will make the car (tyre) skid. We implement and antilock brake system to solve this.

/**
 * Brakes ABS filter
 */
float CarController::FilterABS(float brake)
{
    if (car->_speed_x < ABS_MIN_SPEED)
        return brake;
    float slip = 0.0;
    int i;
    for (i = 0; i < 4; i++){
        slip += car->_wheelSpinVel(i) * car->_wheelRadius(i) / car->_speed_x;
    }
    slip = slip / 4.0;
    if (slip < ABS_SLIP){
        brake = brake * slip;
    }
    return brake;
}

We apply the filter by adding the following line at the end of GetBrake(...) just above the return statement,

float CarController::GetBrake()
{
    // ...
    brake = FilterABS(brake);
    return brake;
}

We also add the new constant to carcontroller.cpp

const float CarController::ABS_SLIP = 0.9; // [-] range [0.95..0.3]
const float CarController::ABS_MIN_SPEED = 3.0; // [m/s]

and we update carcontroller.h with the new class members.

// ...
    public:
        // ...
        static const float ABS_SLIP;
        static const float ABS_MIN_SPEED;

    private:
        // ...
        float FilterABS(float brake);

// ...

Traction Control (TCL)

Suddenly applying full throttle also make the tyres spin. We want to avoid this issue by implementing a traction control mechanism.

/**
 * Acceleration TCL filter
 * Using a pointer to function
 */
float CarController::FilterTCL(float acceleration)
{
    if (car->_speed_x < TCL_MIN_SPEED)
        return acceleration;
    float wheel_speed = (this->*GetDrivenWheelSpeed)();
    float slip = car->_speed_x / wheel_speed;
    if (slip < TCL_SLIP) {
        acceleration = 0.0;
    }
    return acceleration;
}

Select spinning wheel (tyre)'s function to point to:

/**
 * Select the right spinning wheel speed calculation for the TCL filter
 */
void CarController::InitDrivenWheelSpeed()
{
    const char* train_type = GfParmGetStr(car->_carHandle, SECT_DRIVETRAIN, 
                    PRM_TYPE, VAL_TRANS_RWD);
    if(strcmp(train_type, VAL_TRANS_RWD) == 0) {
        GetDrivenWheelSpeed = &CarController::GetWheelSpeedRWD; 
    } else if (strcmp(train_type, VAL_TRANS_FWD) == 0){
        GetDrivenWheelSpeed = &CarController::GetWheelSpeedFWD; 
    } else if (strcmp(train_type, VAL_TRANS_4WD) == 0){
        GetDrivenWheelSpeed = &CarController::GetWheelSpeed4WD;
    }
}

Computing the wheel speed for different train type

/**
 * Get the driven wheel speed for Rear Wheel Drive (RWD)
 */
float CarController::GetWheelSpeedRWD()
{
    return (car->_wheelSpinVel(REAR_RGT) + car->_wheelSpinVel(REAR_LFT)) * 
            car->_wheelRadius(REAR_LFT) / 2.0;
}


/**
 * Get the driven wheel speed for Front Wheel Drive (FWD)
 */
float CarController::GetWheelSpeedFWD()
{
    return (car->_wheelSpinVel(FRNT_RGT) + car->_wheelSpinVel(FRNT_LFT)) * 
            car->_wheelRadius(FRNT_LFT) / 2.0;
}

/**
 * Get the driven wheel speed for Four Wheel Drive (4WD)
 */
float CarController::GetWheelSpeed4WD()
{
    return ( (car->_wheelSpinVel(REAR_RGT) + car->_wheelSpinVel(REAR_LFT)) * 
                car->_wheelRadius(REAR_LFT) +
            (car->_wheelSpinVel(FRNT_RGT) + car->_wheelSpinVel(FRNT_LFT)) * 
                car->_wheelRadius(FRNT_LFT) ) / 4.0;
}

To apply the TCL filter add the following line at the end of GetAcceleration() before the return statement.

float CarController::GetAcceleration()
{
    // ...
    acceleration = FilterTCL(acceleration);
    return acceleration;
}

add InitDrivenWheelSpeed(); in NewRace(...) to set the wheel speed calculation pointer function to the right one.

void CarController::NewRace(tCarElt* car, tSituation* s)
{
    // ...
    InitDrivenWheelSpeed();
}

We define the new constant in carcontroller.cpp,

const float CarController::TCL_SLIP = 0.9; // [-] range [0.95..0.3]
const float CarController::TCL_MIN_SPEED = 3.0; // [m/s]

and add the new class member declaration in carcontroller.h.

// ...
    public: 
        // ...
        static const float TCL_SLIP;
        static const float TCL_MIN_SPEED;

    private:
        // ...
        float FilterTCL(float acceleration);
        float GetWheelSpeedRWD();
        float GetWheelSpeedFWD();
        float GetWheelSpeed4WD();
        void InitDrivenWheelSpeed();

        float (CarController::*GetDrivenWheelSpeed)();
// ...

Test Drive

[Video Link]