Tutorials, Research and Thoughts

Create Your First TORCS Racing AI Bot - Part 01: Acceleration

This is the second chapter of a tutorial for building an AI agent for the racing game TORCS. In this chapter, we will generate the base code of the AI agent, make it drive slowly, and reorganize the code for easier implementation.

Generate the AI agent base code

To start building our AI agent, we will use a tool called robotgen, to create the baseline code structure. robotgen is used as following:

robotgen -n <name> -a <author> -c <car> [-d description] [--gpl]

Where <name> is the name of the agent we are creating, <author> is the name of the creator (AKA you) and <car> is the name of the car we want our agent to be using. We might also use optional argument to give a description of our agent, or add a GPL statement at the beginning of the source code files.

The list of available cars can be obtain as follow:

$ ls /usr/local/share/games/torcs/cars/ 
155-DTM car2-trb1 kc-a110 kc-dbs pw-206wrc 
acura-nsx-sz car3-trb1 kc-alfatz2 kc-dino pw-306wrc 
baja-bug car4-trb1 kc-bigh kc-ghibli pw-corollawrc 
buggy car5-trb1 kc-cobra kc-giulietta pw-evoviwrc 
car1-ow1 car6-trb1 kc-coda kc-grifo pw-focuswrc 
car1-stock1 car7-trb1 kc-conrero kc-gt40 pw-imprezawrc 
car1-stock2 car8-trb1 kc-corvette-ttop kc-gto 
car1-trb1 kc-2000gt kc-daytona kc-p4 
car1-trb3 kc-5300gt kc-db4z p406

Now let's move to the $TOCS_BASE folder, and use the robotgen utility is and generate the initial source code of our agent.

cd $TORCS_BASE 
./robotgen -n "racebot" -a "My racing bot" -c "car1-trb1"

This command will create a folder with the name of your ai agent inside $TORCS_BASE/src/drivers. Inside that folder (in our case $TORCS_BASE/src/drivers/racebot), we will have the following files:

  • Makefile : Instruction for compiling and installing your AI agent
  • racebot.cpp : Actual code of your AI agent
  • racebot.xml : Customizable properties for your agent
  • racebot.def, racebot.dsp : Some Visual Studio (2005) project files. Not much useful anymore
  • car1-trb1.rbg : Image file used as paint job for the car
  • logo.rbg : Image file used as paint job for your driver's name and flag and your team's logo
  • car1-trb1.xcf : GIMP project file to edit your paint job's image files

Install the AI Agent

Let's build and install the AI agent. For that, we move to the directory of our agent, and use the command make.

cd $TORCS_BASE/src/drivers/racebot
make 
make install 

Note: We might need writing permission to the directory where TORCS library are installed /usr/local/lib/torcs/.

Once done, let's test our agent (I am so excited...). Run the game with a double-click on the desktop icon of TORCS or typing the command torcs in the command line. Select Race > Practice > Configure Race. Choose a track: CG Speedway number 1, and click Accept. Click on your AI agent name in the Not Selected column on the right and click (De)Select to put it in the Selected column. Only one participant is allowed in practice session so if there is already another participant in the Selected Box, you have to remove it first by clicking it and click the (De)Select menu item, and then choose your AI agent like we just describe. Then click Accept. After that, select the number of laps and display. We may leave it as is (2 laps). Click Accept, then click New Race in the Practice menu. Wait... Yay our car is on the road!

Car on road

Hey wait! but it is not doing anything ?!...

....That was the joke folks ! (I am sure it bombed)

Yes our AI agent is just chilling because we haven't gave it any instructions. But before we pursue, let's learn to remove our AI agent. First quit the game and then delete the 'installed' driver folder from /usr/local/lib/torcs/drivers/. In our example, we can run:

rm -f /usr/local/lib/torcs/drivers/racebot

but for future easier uninstalls, add the following lines at the end of the Makefile in our AI Agent folder:

uninstall: 
    rm -rf ${libdir}/${MODULEDIR}

From now, can comfortably uninstall by typing make uninstall from the our AI Agent development folder.

Before We Write Some Code

In this chapter of the series, we implement basic steering and acceleration. All in the AI agent main cpp file represented by racebot.cpp. Let's check the content of the file.

Open the file with your favourite text editor, one with syntax highlighting should be enough. The file start with some standard C language header files. Then we have some TORCS header files that includes the necessary data structures and functions for the implementation of our agent. Note: those file are accessible via $TORCS_BASE/export/include.

  • tgf.h : Generic gaming API utilities
  • track.h : Structure and loader of tracks
  • car.h : Data structure (and sub structure) representing a car
  • raceman.h : Data Structures providing information during a race
  • robottools.h : Utility functions to compute values useful to the AI agent (robot)
  • robot.h : AI agent (robot) module interface definition. Contains callback functions that operate the robot.

After the headers come the declaration and definition of the function we will edit to build our AI agent.

initTrack: Called for every track change. Can be used to deal with some initialization before a new race.

static void initTrack(int index, tTrack* track, void *carHandle, void **carParmHandle, tSituation *s); 

newrace: Used as callback when a new race starts.

static void newrace(int index, tCarElt* car, tSituation *s);

drive: Used as callback to get the driving instructions.

static void drive(int index, tCarElt* car, tSituation *s);

endrace: Used as callback when the current race ends.

static void endrace(int index, tCarElt *car, tSituation *s);

shutdown: Called before the module is unloaded. Can be used to free memory.

static void shutdown(int index);

InitFuncPt: Module interface initialization. Setup the implemented callback function that will operate the AI agent.

static int InitFuncPt(int index, void *pt);

racebot (AI agent name): Module entry point. Used to initialize the AI agent module properties.

extern "C" int racebot(tModInfo *modInfo) 
{ 
... 
}

Let's drive

To drive we set the values of our car control. Those values are updated every 0.2 second by the method drive. the control values are: * car->ctrl.steer between -1.0 and 1.0 * car->ctrl.gear between -1 and the car max gear * car->ctrl.accelCmd between 0 and 1.0 * car->ctrl.brakwCmd between 0 and 1.0

To accelerate we just press the accelerator right ?! (set it to 1.0) ... We might as well end up in the grass or the wall a the first turn you encounter! So I think we might want some basic steering. For our first drive the plan is to accelerate slowly and steer in a way to always stay in the middle of the track. My laziness sense tells me that it is time to quote the tutorial shipped with the game.

Before we can start implementing simple steering we need to discuss how the track looks for the robot. The track is partitioned into segments of the following types: left turns, right turns and straight segments. The segments are usually short, so a turn that looks like a big turn or a long straight is most often split into much smaller segments. The segments are organized as linked list in the memory. A straight segment has a width and a length, a turn has a width, a length and a radius, everyting is measured in the middle of the track. All segments connect tangentially to the next and previous segments, so the middle line is smooth. There is much more data available, the structure tTrack is defined in $TORCS_BASE/export/include/track.h.

The image below from Wikipedia give an illustration of the quote above.

Track Segment section

The goal of our simple steering function is to make the car follow the middle of track. For that, we steer the front wheel parallel to the track, and we add a correction value if we are not in the middle of the track.

To know how much we need to steer the wheel, we need to know three things:

  • The angle formed by the middle line of the starting segment of the track and the tangent line to the segment where the car currently is. It is obtained via the function RtTrackSideTgAngleL(&(car->_trPos)) from robottools.h.
  • The angle formed by the middle line of the starting segment of the track and the yaw direction of the car. I am not talking about yawning tho. This is obtained via car->yaw. A yaw rotation is a movement around the yaw axis. The 3 axes of a car, plane have specific names: roll, pitch and yaw. Rotational Axis
  • The distance between the car and the middle line of the segment where the car is. This is obtained via car->_trkPos.toMiddle, which is positive if the car is to the left of the middle line and negative if the car is to the rigth of the middle line. This value is in meter so to contribute to steering angle, it will be normalized by the width of the segment car->_trkPos.seg->width.

Now to compute the steering angle, we substract RtTrackSideTgAngleL(&(car->_trPos)) from car->_yaw, we make sure to make it lies between π and -π, either using NORM_PI_PI(angle) from the game API, or the remainder(double x, double y) function from math.h. Next we substract car->_trkPos.toMiddle (in meters), normalized by car->_trkPos.seg->width from the obtained value. We then convert the angle to the steering range [-1, 1] by dividing the angle by the maximum steering value of the car: car->_steerLock.

We also accelerate slowly (by 30%), with the gearbox set to the first gear. All of this is implemented within the drive function that will look like:

static void drive (int index, tCarElt* car, tSituation *s)
{ 
    memset((void *)&car->ctrl, 0, sizeof(tCarCtrl)); // reset the values

    float angle = RtTrackSideTgAngleL(&(car->_trkPos)) - car->_yaw;
    angle = remainder( angle, 2*PI); // making sure angle is within -PI and PI
    angle -= car->_trkPos.toMiddle / car->_trkPos.seg->width;

    car->_steerCmd = angle / car->_steerLock;
    car->_gearCmd = 1; // first gear
    car->_accelCmd = 0.3; // 30% acceleration
    car->_brakeCmd = 0.0; // no brakes
}

The picture below try to give you a better intuition of what is going on with the angles, and the algorithm above.

Steering Angles

Test drive

[Video Link]

Restructuring the code

Now let's prepare for the coming chapters. We will reorganize the code in a more modular way. We will create a class that will be responsible for controlling our AI agent. Let's call it CarController (yeah, very original).

We create a file named carcontroller.h and define the class. The content of the class are callback methods, some properties of our AI module, and helper methods to compute controls values. We also put the car and the track as members of the class for easier access.

#ifndef _CARCONTROLLER_H_
#define _CARCONTROLLER_H_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include <tgf.h>
#include <track.h>
#include <car.h>
#include <raceman.h>
#include <robottools.h>
#include <robot.h>

class CarController {

    public: 
        CarController();
        void InitTrack(tTrack* track, void* carHandle, void** carParmHandle, 
                        tSituation* situation);
        void NewRace(tCarElt* car, tSituation* s);
        void Drive(tSituation* situation);
        void EndRace(tSituation* situation);
        int PitCommand(tSituation* situation);
        char* GetName();
        char* GetDescription();

    private:
        float GetAcceleration();
        float GetBrake();
        float GetGear();
        float GetSteering(float car_angle);
        float CurrentCarAngle(tSituation* situation);

        tCarElt* car;
        tTrack* track;

        char* name;
        char* description;

};

Create a file called carcontroller.cpp and define and define the methods as follows:

#include "carcontroller.h"


CarController::CarController()
{
    this->name = strdup("racebot");
    this->description = strdup("");
}

/**
 * Callback before the start of every race.
 */
void CarController::InitTrack(tTrack* track, void* carHandle, void** carParmHandle, 
        tSituation* situation)
{
    this->track = track;

    *carParmHandle = NULL; 
}

/**
 * Called at the start of a new race
 */
void CarController::NewRace(tCarElt* car, tSituation* s)
{
    this->car = car;
}

/**
 * Function that setup the controls values to drive the car during the race.
 */
void CarController::Drive(tSituation* situation)
{
    memset((void *)&car->ctrl, 0, sizeof(tCarCtrl)); // reset the values

    float car_angle = CurrentCarAngle(situation);

    car->_steerCmd = GetSteering(car_angle);
    car->_gearCmd = GetGear(); 
    car->_accelCmd = GetAcceleration(); 
    car->_brakeCmd = GetBrake(); 
}

/**
 * Return the name of the controller, to be used as module name.
 */
char* CarController::GetName()
{
    return this->name;
}


/**
 * Return the description of the controller to be used as the module description
 */
char* CarController::GetDescription()
{
    return this->description;
}

/**
 * End of the current race
 */
void CarController::EndRace(tSituation* situation)
{
    // TODO 
}

/**
 * Command for the pitstop
 */
int CarController::PitCommand(tSituation* situation)
{
    return ROB_PIT_IM; // return immediately
}

/**
 * Compute and return the acceleration value to be applied.
 */
float CarController::GetAcceleration()
{
    return 0.3; // 30% acceleration
}

/**
 * Compute and return the braking value to be applied.
 */
float CarController::GetBrake()
{
    return 0.0; // No brakes
}

/**
 * Compute and return the gear to be applied.
 */
float CarController::GetGear()
{
    return 1; // first gear
}

/**
 * Compute and return the steering value.
 */
float CarController::GetSteering(float car_angle)
{
    float steering_angle;
    steering_angle = car_angle - car->_trkPos.toMiddle / car->_trkPos.seg->width;
    return steering_angle / car->_steerLock;
}

/**
 * Computes the current angle of the car relatively to the track
 */
float CarController::CurrentCarAngle(tSituation* situation)
{
    float car_angle = RtTrackSideTgAngleL(&(car->_trkPos)) - car->_yaw;
    car_angle = remainder( car_angle, 2*PI); // ensure it is within -PI and PI
    return car_angle;
}

Let's go back to racebot.cpp, and update it accordingly. First, we include our CarController class header file and define a static variable that will hold an instance of our CarController. Then we instanciate the CarController within the module entry point method racebot(...). You may also notice that the module interface initialization function in InitFuncPt(..) is missing a callback. The itf->rbPitCmd is set to NULL. Let's set it to pitcommand, a callback function that we will define shortly after. The remaining of the code is basically about replacing the callback functions their counterparts from the CarController class. Here is the listing of the updated racebot.cpp, with some formatting.

/** Previous includes **/
#include "carcontroller.h"

static tTrack   *curTrack;
static CarController* controller; // static pointer to the controller

static void initTrack(int index, tTrack* track, void *carHandle, void **carParmHandle, tSituation *s); 
static void newrace(int index, tCarElt* car, tSituation *s); 
static void drive(int index, tCarElt* car, tSituation *s); 
static void endrace(int index, tCarElt *car, tSituation *s);
static void shutdown(int index);
static int  InitFuncPt(int index, void *pt); 


/* 
 * Module entry point  
 */ 
extern "C" int 
racebot(tModInfo *modInfo) 
{
    memset(modInfo, 0, 10*sizeof(tModInfo));

    controller = new CarController(); // Instanciate the controller
    modInfo->name    = controller->GetName(); // Module name  
    modInfo->desc    = controller->GetDescription(); // Module description

    modInfo->fctInit = InitFuncPt;      /* init function */
    modInfo->gfId    = ROB_IDENT;       /* supported framework version */
    modInfo->index   = 1;
    return 0; 
} 

/* Module interface initialization. */
static int 
InitFuncPt(int index, void *pt) 
{ 
    tRobotItf *itf  = (tRobotItf *)pt; 

    itf->rbNewTrack = initTrack; /* Give the robot the track view called */ 
                 /* for every track change or new race */ 
    itf->rbNewRace  = newrace;   /* Start a new race */
    itf->rbDrive    = drive;     /* Drive during race */
    itf->rbPitCmd   = NULL;
    itf->rbEndRace  = endrace;   /* End of the current race */
    itf->rbShutdown = shutdown;  /* Called before the module is unloaded */
    itf->index      = index;     /* Index used if multiple interfaces */
    return 0; 
} 

/* Called for every track change or new race. */
static void  
initTrack(int index, tTrack* track, void *carHandle, void **carParmHandle, tSituation *s) 
{ 
    controller->InitTrack(track, carHandle, carParmHandle, s);
} 

/* Start a new race. */
static void  
newrace(int index, tCarElt* car, tSituation *s) 
{ 
    controller->NewRace(car, s);
} 

/* Drive during race. */
static void  
drive(int index, tCarElt* car, tSituation *s) 
{ 
    controller->Drive(s);
}

/* End of the current race */
static void
endrace(int index, tCarElt *car, tSituation *s)
{
    controller->EndRace(s);
}

/* Called before the module is unloaded */
static void
shutdown(int index)
{
    delete controller;
}

Compile and run it to ensure that everything went smoothly. If not and you are on the edge of tearing of your hairs out of your head, please download or clone this code and copy the racebot-01 folder into the $TORCS_BASE/drivers folder and compile it.

Here is a test drive of the result.

That's it for this chapter!... And yes we have a working agent! But let's be honest it is kind of lame. So let's make him faster, break, and change gears.