Car AI

I know that many of you want to create racing games, so I have decided to start this new car AI series. This month we will learn to create a car that uses a path, accelerates and decelerates, stops after a predefined number of laps, includes a 3D sound whose frequency is tuned depending on the speed of the car... Oh, and we will learn to create a timer that displays seconds and milliseconds!

Let's start with a text and a panel definition:

text info_txt
{
     font = system_font;
     pos_x = 5;
     pos_y = 5;
     string = "Lap:   Time:";
     flags = visible;
}

panel info_pan
{
     pos_x = 0;
     pos_y = 0;
     layer = 10;
     digits = 45, 5, 1, system_font, 1, laps_scr;
    // digits = 125, 5, 3.3, system_font, 1, total_time; // A6 version
     digits = 125, 5, 3, system_font, 1, total_time; // // A5 version
     flags = overlay, refresh, visible;
}

The text displays "Lap:" and "Time:" at the top of the screen and the panel displays the figures. The first "digits" line displays the number of laps and the second "digits" line displays the time in seconds (A5 version) and seconds + milliseconds (A6 version). Remove the comment from the "A6 version" line and comment the "A5 version" line if you own A6.

function main()
{
     fps_max = 30;
     level_load (carai_wmb);
     while (1)
     {
          if (key_c != 1)
          {
               camera.x = -3400;
               camera.y = 1325;
               camera.z = 840;
               camera.pan = 326;
               camera.tilt = -17;
          }
          else
          {
               camera.x = car1.x - 150 * cos(car1.pan);
               camera.y = car1.y - 150 * sin(car1.pan) ;
               camera.z = car1.z + 30;
               camera.pan = car1.pan;
               camera.tilt = 0;
          }
          if (car1.race_over == 0)
          {
               total_time = total_frames / 30; 
          }
          wait (1);
     }
}

Function main limits the frame rate to 30 fps and then loads the level. My demo uses two cameras: press and hold the "C" key to switch to a camera that chases the car, or release the "C" key to switch to a fixed, isometric camera view. As soon as you press the "C" key, the camera will be placed 150 quants behind the car and 30 quants above it.

The time is computed and stored in total_time for as long as the car continues to move (car1.skill20 aka car1.race_over isn't set to zero). Let's see how this simple timer formula works:

total_time = total_frames / 30;

We have limited fps_max to 30, right? This means that on a decent pc the engine will render 30 frames each second. The predefined variable named total_frames holds the number of frames that have passed since the engine was started. Let's see a few possible values:

total_frames =  0   ->   total_time = 0.000
total_frames =  1   ->   total_time = 0.033
total_frames =  2   ->   total_time = 0.066
total_frames =  3   ->   total_time = 0.100
total_frames =  4   ->   total_time = 0.133

............................................................

total_frames = 29  ->   total_time = 0.966
total_frames = 30  ->   total_time = 1.000
total_frames = 31  ->   total_time = 1.033

...........................................................

This way you get a decent timer using a single line of code. Let's examine the action attached to the car:

action car
{
     car1 = me;
     my.race_over = 0;
     var waypoint_number = 0;
     var car_speed;
     var target_coords; // the coordinates of the next waypoint on the path
     var engine_handle;
     var angle_diff;
     my.counting_laps = 1;
     my.laps = -1;
     if (my.skill1 == 1)
     {
          ent_path("car01");
     }
     engine_handle = ent_playloop (my, engine_wav, 50);
     ent_waypoint(target_coords, 1);

I have decided to use a pointer for the car because I wanted to be able to chase it with the camera. Every car will have a skill defined as race_over that will be set to 0 at game start and to 1 when the car finishes the race. There are a few more local variable definitions, but we'll see what they do a little later. If the car has its skill1 set to 1, it will follow the path named "car01" in Wed. The car will play the sound named engine_wav in a loop, and the last line of code sets target_coords to the first point on the "car01" path.

     while ((waypoint_number < 300) && (my.race_over == 0))
     {
          temp.x = target_coords.x - my.x;
          temp.y = target_coords.y - my.y;
          temp.z = 0;
          if (vec_to_angle (angle_diff.pan, temp) < 150)
          {
               waypoint_number = ent_nextpoint(target_coords);
          }
          angle_diff.pan = ang(angle_diff.pan - my.pan);
          my.pan += car_speed * angle_diff.pan * 0.005;

My path has less than 300 points, so "while (waypoint_number < 300)" is the same thing with "while (1)", isn't it? The car will continue to move until my.race_over will be set to 1, and guess what? This will happen at the end of the race! We store the vector from the current position of the car (my.x and my.y) to target_coords (first point on the path) in temp, and then we rotate the car towards the next point on the path. The next destination point will be set when the car comes closer than 150 quants to its current destination.

We increase the pan angle of the car smoothly, depending on angle_diff.pan. You can play with 0.005, but please be aware that if the speed of the car is too big and 0.005 was set to a smaller value, the car might not have enough time to rotate towards its destination point.

          if (((waypoint_number > 65) || (waypoint_number < 8)) || ((waypoint_number > 34) && (waypoint_number < 49)))
          {
               if (car_speed.x < 60 * time)
               {
                    car_speed.x += 1 * time;
               }
          }
          else
          {
               if (car_speed.x > 40 * time)
               {
                    car_speed.x -= 0.5 * time;
               }
          }

The lines of code above accelerate and decelerate depending on the configuration of the track; you shouldn't expect a car driven by the computer to "know" where and why to reduce its speed, right? Take a look at the picture below:
 

The image shows the relevant points on the "car01" path. The car will accelerate until its speed reaches 60 * time if it moves inside one of these regions: 65...8 or 34...49; else (8...34 or 49...65) the car will decrease its speed until it reaches 40 * time.

          snd_tune (engine_handle, 50, car_speed.x * 5, 0);
          car_speed.y = 0;
          car_speed.z = 0;
          move_mode = ignore_you + ignore_passable + glide;
          ent_move(car_speed, nullskill);
          vec_set (temp, my.x);
          temp.z -= 1000;
          trace_mode = ignore_me + ignore_passable + scan_texture;
          trace (my.x, temp);
          if (str_cmpi ("metalplainl1", tex_name))
          {
               add_laps();
          }
          if (my.laps == 3)
          {
               my.race_over = 1; // get out of the while loop after 3 laps
          }
          wait(1);
      }

The car will tune its sound depending on the value set for car_speed.x; we trace below the car in order to get the name of the texture used for the road. If that name is "metalplainl1", the car has completed a lap so we must run the function named add_laps(). If the number of laps for the car is 3, we set race_over to 1, in order to get out of the first while loop.

     while (car_speed.x > 1)
     {
          car_speed.x -= 1 * time;
          snd_tune (engine_handle, 50, car_speed.x * 5, 0);
          car_speed.y = 0;
          car_speed.z = 0;
          move_mode = ignore_you + ignore_passable + glide;
          ent_move(car_speed, nullskill);
          wait (1);
     }
     snd_stop (engine_handle);
}
 
The while loop above decreases the speed of the car until it goes below 1 * time and tunes the sound accordingly. The car will move slower and slower until it stops completely, because the while loop stops running. The car engine sound will be shut down as well, and we will take a look at the last function:

function add_laps()
{
     if (my.counting_laps == 0) {return;}
     my.counting_laps = 0;
     my.laps += 1;
     snd_play (lap_wav, 100, 0);
     laps_scr = my.laps;
     sleep (10);
     my.counting_laps = 1;
}

The car could detect metalplainl1 below its wheels for more than a frame, especially if its speed is small, and this would translate to several add_laps() calls every lap. The solution is easy: we set counting_laps = 0 after the first function call, and we enable the function again only after 10 seconds, when counting_laps = 1. This way we can increase the number of laps for the car (my.laps += 1), we can play a sound (lap_wav) and we can display the number of laps on the screen (laps_scr) only once per lap, provided that the car needs less than 10 seconds to move over the metalplainl1 block.

Some of you might have noticed an isolated level block in Wed's top view:

That's the block that was used for all the curved portions of the track; I have rotated copies of it with 15, 30, 45, 60 ... degrees and I thought that you might want to use it as a base if you plan to modify my track. Next month we will learn to create several cars controlled by the computer and we will see them interacting with each other, so stay tuned!
 
 

Player selection

This article will help you to create a player selection menu that includes 3 different models, each one of them having 3 different skins. You choose the desired model using the left and right arrows, and then you choose the skin using the up and down arrows. Press Enter to start a level that uses the player you've selected when you are happy with your selection.

This is a standalone project, but function main is really simple:

function main()
{
     on_d = null;
     fps_max = 60;
     level_load (dummy_wmb);
}

We load a level named dummy_wmb; this level is a simple hollowed cube with the 3 models placed inside it.

I have used a small light that was placed close to the floor, so the level looks completely dark, although the texture used for the cube isn't black.

Each model has this action attached to it:

action rotate_model
{
     my.skin = 1;
     my.light = on;
     my.lightred = 155;
     my.lightgreen = 155;
     my.lightblue = 155;
     my.lightrange = 0;

Every model displays its first skin at game start; the models have their "light" flag set, so they will glow in the dark because of their lightred, lightgreen and lightblue values, even if their lightrange is set to zero.

     while (game_started == 0)
     {
          my.pan += 3 * time;
          if (key_cuu == 1)
          {
               while (key_cuu == 1) {wait (1);}
               my.skin += 1;
               if (my.skin == 4) {my.skin = 1;}
          }
          if (key_cud == 1)
          {
               while (key_cud == 1) {wait (1);}
               my.skin -= 1;
               if (my.skin == 0) {my.skin = 3;}
          }
          if (key_cur == 1)
          {
               while (key_cur == 1) {wait (1);}
               camera.pan -= 40;
          }
          if (key_cul == 1)
          {
               while (key_cul == 1) {wait (1);}
               camera.pan += 40;
          }

The while loop above will continue to run for as long as game_started remains zero. The models will rotate around their pan angle; if we press the up arrow key, we wait until the key is released and then we set the next skin for the model. The same thing happens when we press the "down" key; please note that we have to limit my.skin to 1..3 because every model has 3 skins.

If we press the right arrow key, we wait until it is released and then we subtract 40 degrees from camera.pan, in order to display the next model. Take a look at the picture below:


 

Our mission is simple: we have to rotate the camera with 120 degrees if we want to be able to see (and select) a new model. What is with those 40 degrees then? Well, we have 3 models, and each one of them has the action attached to it, so when we press the right arrow key, the camera is rotated with 40 + 40 + 40 = 120 degrees, get it? The same thing happens when we press the left arrow key (in a reversed direction, of course).

          camera.pan %= 360;
          if (key_enter == 1)
          {
               game_started = 1;
               player_model = camera.pan / 120 + 1;
               player_skin = my.skin;
               start_game();
          }
          wait (1);
     }
}

We need to limit camera.pan to 0...359 degrees (you'll see why in a little while) and then we check if the Enter key was pressed. If this is true, we set game_started to 1 in order to get out of this while loop and then we set player_model to 1, 2 or 3, depending on the angle of the camera (0, 120 or 240). Finally, we set a variable named player_skin to the number of the skin that was selected by the player, and then we call the function named start_game():

function start_game()
{
     level_load (level_wmb);
     wait (3);
     if (player_model == 1)
     {
          player = ent_create (cbabe_mdl, start_pos, player_moves);
     }
     if (player_model == 2)
     {
          player = ent_create (guard_mdl, start_pos, player_moves);
     }
     if (player_model == 3)
     {
          player = ent_create (hero_mdl, start_pos, player_moves);
     }
}

This time we load the real level and then we create a player model depending on the value that was stored in player_model; the function that drives the model is named player_moves():

function player_moves()
{
     my.skin = player_skin;
     while (1)
     {
          my.skill1 = 5 * (key_w - key_s) * time;
          my.skill2 = 0;
          my.skill3 = 0;
          my.pan += 3 * (key_a - key_d) * time;
          move_mode = ignore_passable + ignore_passents + glide;
          ent_move(my.skill1, nullvector);
          if (key_w + key_s > 0)
          {
               ent_cycle("walk", my.skill46);
               my.skill46 += 5 * time;
               my.skill46 %= 100;
           }
          camera.x = my.x;
          camera.y = my.y;
          camera.z = my.z + 200;
          camera.tilt = -90;
          wait (1);
     }
}

The first line of code sets the correct player skin, depending on the value that was stored in player_skin; the player can move using the WSAD keys. If the player is walking (W or S are pressed), it plays its "walk" animation in a loop. The camera is placed 200 quants above player's origin and inherits its x and y. Take a look at the shot below; it was taken from the playable level.