AI - part 1

Top  Previous  Next

Welcome to a new workshop! This month we are going to start a fresh series that discusses artificial intelligence in detail, giving plenty of fully functional examples along the way. We are going to start with several simple, easy to understand snippets, and then we will move on to more advanced AI systems.

 

The most simple AI snippets don't deal with obstacle avoidance at all and work great for games where the player and the enemy have to fight in an open field (a big plain, etc). Basically, we want to have an enemy that can "see" the player, starting to chase it as soon as that happens.

 

Load the ai1.c script and then run it; you should see something that looks like this:

 

aum106_ai1

 

This is our virtual playground; you're going to see it quite a bit! Move towards the friendly enemy (use the WSAD keys and the mouse to do that) and you'll discover that it will turn, and then move towards you as soon as you come closer than a certain distance to him (500 quants in this demo). The enemy will stop when it is close enough to you, playing its "stand" animation. Oh, and since it is a friendly enemy, so it won't harm you.

 

aum106_ai2

 

Let's take a good look at the code that makes it all happen, shall we?

 

function main()

{

       camera.ambient = 70;

       fps_max = 70;

       video_mode = 8;

       video_screen = 1; // start in full screen mode

       level_load (ai1_wmb);

       wait (2);

       ent_createlayer("skycube+6.tga", SKY | CUBE | SHOW, 1);        

}

 

Function main sets the camera ambient, the frame rate, the screen resolution and mode, and then loads the level. We wait until the level is loaded, and then we create the sky cube that will surround our level.

 

action players_code() // attach this action to your player model

{        

       var movement_speed = 20;

       VECTOR temp;

       set (my, INVISIBLE);

       player = my;

       while (1)

       {

               my.pan -= 7 * mouse_force.x * time_step;

               temp.x = movement_speed * (key_w - key_s) * time_step;

               temp.y = movement_speed * (key_a - key_d) * 0.6 * time_step;

               temp.z = 0;

               c_move (my, temp.x, nullvector, IGNORE_PASSABLE | GLIDE);

               camera.x = my.x;

               camera.y = my.y;

               camera.z = my.z + 30;

               camera.pan = my.pan;

               camera.tilt += 5 * mouse_force.y * time_step;

               wait (1);

       }

}

 

The player code is really simple; we are using an invisible player that can move using the WSAD keys, ignoring the passable entities and gliding along the level surfaces whenever it is needed. The camera is placed at player's position, 30 quants above its origin. While camera.pan changes together with player's pan, camera's tilt angle is independent of player's tilt angle; this allows us to look up and down without tilting the player model - that would look quite funny if the model would be visible.

 

OK, so now we can finally take a good look at the enemy code:

 

function enemy_stands()

{

       ent_animate(my, "stand", stand_percentage, ANM_CYCLE); // then play the "stand" animation

       stand_percentage += 2 * time_step; // 2 sets the "stand" animation speed        

}

 

The tiny function above will simply animate the enemy while it is standing, with a speed given by 2 * time_step

 

action enemy_1()

{

       VECTOR temp;

       // wait until the player model is loaded

       while (!player) {wait (1);}

       while (1)

       {

               if (vec_dist(player.x, my.x) > 500) // the player is more than 500 quants away from the enemy

               {

                       enemy_stands(); // play enemy's "stand" animation

               }

               else // the player has come closer than 500 quants to the enemy here

               {

                       if (vec_dist (player.x, my.x) > 150) // the enemy didn't come close enough to the player yet?

                       {

                               vec_set(temp, player.x);

                               vec_sub(temp, my.x);

                               vec_to_angle(my.pan, temp);

                               ent_animate(my, "run", run_percentage, ANM_CYCLE); // then play the "run" animation

                               c_move (my, vector(10 * time_step, 0, 0), nullvector, IGNORE_PASSABLE | GLIDE);                                

                               run_percentage += 6 * time_step; // 6 = "run" animation speed                                

                       }

                       else // the enemy is close enough to the player here

                       {

                               enemy_stands(); // so let's play its "stand" animation

                       }

               }

               wait (1);

       }

}

 

As you can see, the code that is used by our enemy is very simple; it waits until the player model is loaded in the level, and then it waits until the player comes closer than 500 quants to the enemy. Meanwhile, the enemy will play its "stand" animation.

 

As soon as the player comes close enough, the enemy starts moving towards it; the first 3 lines inside the "if" branch rotate the enemy towards the player, while the rest of the code will animate the enemy using its "run" animation and will move it towards the player. The chasing will stop as soon as the distance between the player and the enemy gets smaller than 150 quants; we don't want the enemy to keep bumping into the player.

 

I'd say that we're doing quite a few things with only 15-20 lines of code; still, I can see a few things that could be improved. First of all, our enemy can detect the player even if it approaches from behind, something very unlikely to happen in a commercial quality game. The reason for this is simple: we are using a simple vec_dist instruction, but hopefully there's an easy fix: the c_scan instruction.

 

The second issue that bothers me a bit has to do with the way in which the enemy turns towards the player; we need to make the rotation much smoother. The good news is that our next snippet solves both these problems - take a look!

 

action players_code() // attach this action to your player model

{        

       var movement_speed = 30;

       VECTOR temp;

       set (my, INVISIBLE);

       player = my;

       my.emask |= ENABLE_SCAN;

       while (1)

       {

               my.pan -= 7 * mouse_force.x * time_step;

               temp.x = movement_speed * (key_w - key_s) * time_step;

               temp.y = movement_speed * (key_a - key_d) * 0.6 * time_step;

               temp.z = 0;

               c_move (my, temp.x, nullvector, IGNORE_PASSABLE | GLIDE);

               camera.x = my.x;

               camera.y = my.y;

               camera.z = my.z + 30;

               camera.pan = my.pan;

               camera.tilt += 5 * mouse_force.y * time_step;

               wait (1);

       }

}

 

I have added a single line of code to player's action, the one in bold, which makes the player sensitive to the enemy's c_scan instruction; otherwise, the player will not be detected.

 

function enemy_stands()

{

       ent_animate(my, "stand", stand_percentage, ANM_CYCLE); // then play the "stand" animation

       stand_percentage += 2 * time_step; // 2 sets the "stand" animation speed        

}

 

The enemy_stands function was already perfect ;) so I didn't modify it at all.

 

action enemy_1()

{

       VECTOR temp, enemy_target;

       ANGLE temp_angle;        

       // wait until the player model is loaded

       while (!player) {wait (1);}

       while (1)

       {

               c_scan(my.x, my.pan, vector(120, 60, 1000), IGNORE_ME | SCAN_ENTS | SCAN_LIMIT);

               if (you == player)                

               {

                       if (vec_dist (player.x, my.x) > 150) // the enemy didn't come close enough to the player yet?

                       {

                               vec_set(enemy_target, player.x);

                               vec_sub(enemy_target, my.x);

                               vec_to_angle(temp_angle, enemy_target);

                               my.pan += ang(temp_angle.pan - my.pan) * 0.25 * time_step;

                               ent_animate(my, "run", run_percentage, ANM_CYCLE); // then play the "run" animation

                               c_move (my, vector(10 * time_step, 0, 0), nullvector, IGNORE_PASSABLE | GLIDE);                                

                               run_percentage += 6 * time_step; // 6 = "run" animation speed                                

                       }

                       else // the enemy is close enough to the player here

                       {

                               enemy_stands(); // so let's play its "stand" animation

                       }

               }

               else // the player wasn't detected

               {

                       enemy_stands(); // so let's play the enemy's "stand" animation                        

               }

               wait (1);

       }

}

 

The function that drives the enemy is surprisingly short again; we are now using a c_scan instruction that scans using a horizontal range of 120 degrees, a vertical range of 60 degrees and a distance of up to 1,000 quants. If the player entity is detected (don't forget to put a "player = my;" line of code inside your own player action), we compute the target position each and every frame, and then we modify the enemy's pan angle gradually, depending on the difference between its pan angle and the target angle, with a speed given by 0.25 * time_step. That's all there is to know about this snippet, because the rest of the code is identical.

 

OK, so now we've got an enemy that becomes alert when it "sees" the player, rotates towards the player smoothly and starts chasing it, but I feel that we're still missing something. I mean, we have talked about this guard as being the enemy, but it doesn't attack the player at all! Sure, it's got that frowny look on its face, but that look won't hurt anyone... excepting some of our feelings, maybe. Since it is an enemy, shouldn't we see this brave guy attacking the player? If you answered a sound YES! at the previous question, fire up the ai3 demo because the things are going to become very dangerous fast!

 

aum106_ai3

 

Player's action has remained the same, so I won't discuss it again.

 

function enemy_stands()

{

       ent_animate(my, "stand", stand_percentage, ANM_CYCLE); // then play the "stand" animation

       stand_percentage += 2 * time_step; // 2 sets the "stand" animation speed        

}

 

The same thing goes for the enemy_stands function, which tells the enemy to play its "stand" animation

 

function enemy_attacks()

{

       ent_animate(my, "attack", attack_percentage, ANM_CYCLE); // play the "attack" animation

       attack_percentage += 6 * time_step; // 6 sets the "attack" animation speed        

}

 

The function above is very similar with enemy_stands; it plays the enemy's attack animation, with a speed that is given by 6 * time_step.

 

function attach_mace()

{

       proc_mode = PROC_LATE;

       set (my, PASSABLE);

       while (you)

       {

               vec_set(my.x, you.x);

               vec_set(my.pan, you.pan);

               my.frame = you.frame;

               my.next_frame = you.next_frame;                                        

               wait(1);

       }

}

 

Finally, some new code to look at! Function attach_mace keeps the mace in sync with the enemy. The first line of code will eliminate any jerky movements (lags); the mace will be made passable and will have the same position, angles and animation frames with the enemy. This doesn't happen automatically; the enemy and its mace were animated together in a model creation application, and then they were saved as separate pieces.

 

action enemy_1()

{

       VECTOR temp, enemy_target;

       ANGLE temp_angle;        

       // wait until the player model is loaded

       while (!player) {wait (1);}

       ent_create("mace.mdl", my.x, attach_mace);

       while (1)

       {

               c_scan(my.x, my.pan, vector(120, 60, 1000), IGNORE_ME | SCAN_ENTS | SCAN_LIMIT);

               if (you == player)                

               {

                       if (vec_dist (player.x, my.x) > 120) // the enemy didn't come close enough to the player yet?

                       {

                               vec_set(enemy_target, player.x);

                               vec_sub(enemy_target, my.x);

                               vec_to_angle(temp_angle, enemy_target);

                               my.pan += ang(temp_angle.pan - my.pan) * 0.25 * time_step;

                               ent_animate(my, "run", run_percentage, ANM_CYCLE); // then play the "run" animation

                               c_move (my, vector(10 * time_step, 0, 0), nullvector, IGNORE_PASSABLE | GLIDE);                                

                               run_percentage += 6 * time_step; // 6 = "run" animation speed                                

                       }

                       else // the enemy is close enough to the player here

                       {

                               vec_set(temp, player.x);

                               vec_sub(temp, my.x);

                               vec_to_angle(my.pan, temp);

                               my.tilt = 0;

                               enemy_attacks(); // so let's play the enemy's "attack" animation                        

                       }

               }

               else // the player wasn't detected

               {

                       enemy_stands(); // so let's play the enemy's "stand" animation                        

               }

               wait (1);

       }

}

 

The enemy action remains surprisingly short; the new or edited lines are bold once again. First of all, we create the mace model, attaching it the attach_mace function we just discussed about. Then, I lowered the distance which was supposed to stop the enemy from moving towards the player from 150 to 120 quants - it looks better when the enemy uses its mace this way.

 

Finally, since the enemy is supposed to attack the player when it is close to the player, we use vec_to_angle to rotate it towards its target for as long as the player is a reachable target. These vec_to_angle instructions can also change the enemy's tilt angle, so we set the enemy's tilt to zero; we don't want it to sink into the ground or start flying. Finally, we call the enemy_attacks function, which will display the "attack" animation.

 

OK, so we've got an enemy that is furiously attacking the player now, but (fortunately) the player can't die yet. I can't promise that this won't change next month, though, so I'll see you all then!