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!
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.
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))
fromrobottools.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 viacar->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. - 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 segmentcar->_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.
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.