AI - part 7

Top  Previous  Next

Howdy, fellow Gamestudio users! This month brings a new set of exciting features to our AI code:

- Faster code

- The enemy doesn't just rely on nodes to spot and shoot the player. In addition to this, he can also detect and shoot the player from a big distance.

- The path guiding models are gone for good; everything is done using vectors now.

 

I realize that you didn't memorize how my code works, so don't worry - we will explain it all here. And since the enemy code is the one that's significantly changed, we will discuss it all. But first, here's our enemy spotting and shooting our player while moving from one node to the other. Gone are the days when you were able to kill it by simply catching it while it was moving from point A to point B on the path. No sir, this enemy will now stop if it detects the player and start shooting it. In fact, it will only ask for a new path when the player is out of sight.

 

aum112_ai1

 

OK, time to take a good look at this month's enemy code; believe it or not, it does more intelligent things while using fewer lines of code.

 

action my_enemy()

{

       var anim_percentage;

       var shoot_percentage;

       VECTOR temp, marker_pos, bullet_coords;

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

       while (distances_computed == 0) {wait (1);} // wait until all the distances are computed

       my.skill47 = 5678; // I'm a enemy, used in the trace_back function

       my.skill88 = 100; // store enemy's health inside its own skill88

 

This fellow waits until the player pointer is set (the player model is loaded) and then waits until all the distances between all the consecutive nodes are loaded. I was using a "fps_max = 60;" instruction to keep the frame rate under control, but then I thought that I'd better set fps_max to 60 after the distances are computed; this way, the distances will be computed much faster, at game start, when nobody cares about an extra second loading time.

 

We have also got two skills being used here; skill47 is set to a weird value (we want to have the ability of uniquely identifying an enemy) and skill88 stores the enemy's own health value.

 

       while (player)

       {

               if (c_trace (my.x, player.x, IGNORE_ME | IGNORE_MODELS | IGNORE_PASSENTS) == 0)

               {

                       if (my.skill88 < 0) break; // get out of this loop if the enemy is dead                        

                       vec_set(temp, player.x); // turn towards the player

                       vec_sub(temp, my.x);

                       vec_to_angle(my.pan, temp);                

                       my.tilt = 0;        

 

The while loop above runs only while the player is alive; we don't want to run complicated functions is there's nobody to benefit from their result. We perform a c_trace instruction from the enemy to the player, ignoring the enemy model, other models and passable entities. If the result is zero (the enemy can see the player) we rotate the enemy model towards player's model. The "if (my.skill88 < 0) break;" line of code tells the enemy to get out of while loop when its health goes below zero, thus ending its function.

               

                       if (players_health > 0) // shoot bullets only if the player is alive

                       {                        

                               anim_percentage += 6 * time_step; // 6 = shoot animation speed

                              ent_animate(my, "standshoot", anim_percentage, ANM_CYCLE);

                               if(anim_percentage % 100 < 2) //it's the beginning of the "shoot" animation?

                               {

                                        vec_for_vertex(bullet_coords, my, 7); // get the position of the 7th vertex as the bullet origin

                                         {

                                               ent_create (bullet_mdl, bullet_coords, move_bullets);

                                               snd_play(bullet_wav, 50, 0);

                                       }                                        

                               }

                       }

                       else

                       {

                               anim_percentage += 3 * time_step; // 3 = stand animation speed

                            ent_animate(my, "stand", anim_percentage, ANM_CYCLE);                                        

                       }

               }

 

The enemy will only shoot bullets if the player is alive; if this is the case, the enemy will play its "standshoot" animation in a loop. When we reach 2% from the "standshoot" animation, we fire a bullet using the enemy's 7th vertex coordinates as the bullet origin; otherwise, if the player is dead, the enemy will play its "stand" animation.

 

Why did we pick the 7th vertex, you ask? Well, that's the tip of the gun for this particular enemy model. Since the enemy holds the gun using its right arm, you may run into situations like these:

 

aum112_ai2

 

As you can see, the enemy has spotted the player and it is firing at you, but the bullets don't hit the target, because they hit the wall. The problem can be easily solved by shooting from  the enemy's origin, but that would not look too natural. With the current setup, the things take place just like in real life; if the gun can't "see" you, it can't harm you. Of course, the bullets will easily kill you on the left side of the wall, because the enemy's arm would easily reach the player (you'd see its right arm before seeing its body).

 

Did I mention that the enemy has gotten much smarter? I'm pretty sure that you have figured that out, because (as mentioned above) it doesn't just rely on the existing nodes to detect the player. And since the player used to die from a single enemy shot, I have decided to just play a "beep" sound, rather than kill the player in this demo, and thus force you to restart the project over and over.

 

               else

               {

                       min_enemy = 999999;

                       c_scan(my.x, my.pan, vector(360, 360, 800), IGNORE_ME | SCAN_ENTS); // 800 = scanning range

                       wait (3);

                       k = 0;

                       while (k < max_nodes)

                       {

                               path[k] = 0; // reset the array, get rid of the previously stored path

                               k += 1;

                       }

                       k = 0; // start with the first element of path[30]

                       path[k] = target_node; // write the first path element

                       if (start_node != target_node)

                       {

                               find_path(start_node, target_node); // find the shortest path between these two nodes

                               wait (3); // wait for the path to be computed

                               i = k; // k = number of nodes on the shortest path

 

The "else" branch above runs when the enemy can't spot the player because it is either far away, or because (just like any other brave guy) it is hidden behind a wall. If this is the case, we set the minimum distance from the enemy to its surrounding nodes to a huge value; this way, we are practically deleting the previous values (a variable reset, and thus a value of zero would make the enemy think that the new distance has the absolute minimum value, rather than tell it the variable was reset).

 

The following c_scan instruction scans the entities (the nodes) around the enemy, using a range of 800 quants. You could use larger values here, in case that you want to place fewer nodes in the level, but I'd rather have more nodes and thus smoother paths, even if this will require a bit more CPU power.

 

The "path" array stores the actual enemy path; the content of the array changes each time the enemy requests a new path that leads to the player. This means that the array needs to be reset before using it; otherwise, we could end up storing outdated info in some of its elements.

 

The first path element is always "target_node", so we write it at the very beginning of the "path" array; then, if "start_node" (the node that is the closest to the enemy and can see the enemy) and "target_node" (the node that is closest to the player and can see the player) are different, we call the good old path finding function, which didn't change at all, so we won't discuss it here.

 

Finally, we define a variable named i; it's an index variable, which will allow the enemy to move from one point of the path to the other.

                               

                               while (i >= 0) // decrease i until it goes below zero

                               {

                                       marker_pos.x = node_coords[path[i]][0]; // get the previously stored coordinates for this node

                                       marker_pos.y = node_coords[path[i]][1]; // on x and y

                                       marker_pos.z = my.z; // use enemy's height for z

                                                                       

                                       while (vec_dist(marker_pos.x, my.x) > 10)

                                       {

                                               vec_set (temp.x, marker_pos.x);

                                               vec_sub (temp.x, my.x);

                                               vec_to_angle (my.pan, temp); // turn the enemy towards the path node

                                               my.tilt = 0;

 

The "while" loop above runs until all the nodes on the computed path are visited. The coordinates of each node are stored in the "node_coords" array; we don't want to alter them, because we will reuse them as a result of other path finding function calls, so we copy their coordinates to the marker_pos vector. We're only storing the x and y coordinates of the nodes, so we use the enemy's own z to fill in the marker_pos.z value. Then, we start a new loop which will move the enemy towards the node until the distance between its coordinates and the node coordinates is smaller than 10 quants.

 

All this time we are constantly rotating the enemy towards the node; if we'd only do this once, the enemy could miss the node because it has glided along a bit due to a poorly placed level wall, for example. Also, we are constantly setting the enemy's tilt angle to zero; it would look a bit awkward to see it tilted.

                                               

                                               if (my.skill88 < 0) break; // get out of this loop if the enemy is dead

                                               c_move (my, vector(10 * time_step, 0, 0), nullvector, IGNORE_PASSABLE | IGNORE_FLAG2 | GLIDE); // 10 = movement speed

                                               anim_percentage += 7 * time_step; // 7 = run animation speed

                                               ent_animate(my, "run", anim_percentage, ANM_CYCLE);

 

As you might have noticed, we are running quite a few "while" loops here, and these loops need to be stopped as quickly as possible if the enemy dies. The first line of code above takes care of that through a "break" instruction, which will make the engine get out of the current "while" loop. But let's assume that the enemy isn't dead yet; if this is the case, it will move towards the player, playing its "run" animation in a loop.

 

                                               // scan for the player while moving from one node to the other

                                               c_scan(my.x, my.pan, vector(180, 60, 1000), IGNORE_ME | IGNORE_WORLD | IGNORE_MAPS | SCAN_ENTS | SCAN_FLAG2);

                                               if (you == player) // detected the player?

                                               {

                                                       if (c_trace (my.x, player.x, IGNORE_ME | IGNORE_MODELS | IGNORE_PASSENTS) == 0) // and the enemy can see the player?

                                                       {                                                        

                                                               i = 0; // ignore the nodes on the rest of the previously computed path

                                                               break; // and then get out of this loop

                                                       }

                                               }

                                               wait (1);

                                       }

                                       i -= 1;

                                       wait (1);

                               }

                       }        

               }

               wait (1);        

       }

 

The previous AI code version had a major flaw; the enemy was unable to detect (and thus shoot at) the player before reaching the last node on the path. The things have changed for the better, though; the enemy now scans for the player, trying to detect the player even while it is traveling from one node to the other. To do that, it scans for the entities that have their FLAG2 set, and guess what - only the player has that flag set.

 

This approach "fixes" c_scan's useful feature of detecting the closest entity to it, which (if it weren't for FLAG2) could have been a node, for example. If the player is detected, the enemy traces from it towards the player; if the two entities can see each other, the enemy sets i to zero, telling the code that it has reached the end of the path, and then the "break" instruction is run, stopping the "while" loop and thus giving the enemy the ability to shoot at the player.

 

If you see a lonely "i -= 1;" instruction, that is supposed to run if the player has reached a node on its path without detecting the player, so it can start moving towards the following node on the path.

 

       anim_percentage = 0;

       snd_play(death_wav, 40, 0);

       while (anim_percentage < 90)

       {

               ent_animate(my, "deatha", anim_percentage, ANM_CYCLE); // play the "death" animation

               anim_percentage += 2 * time_step; // sets the death animation speed

               wait (1);

       }

       set (my, PASSABLE);

}

 

You did it! If the snippet above is run, you have killed the enemy! The code is dead ;) simple; it plays a "death_wav" sound, and then it plays the death animation frames. Finally, the corpse is made passable; this way, the player can pass through it without tripping.

 

There are a few more changes in the other scripts, but they're not complicated at all, so you should be able to figure out what's changed on your own. I'll see you all next time, when we'll hopefully have an even more intelligent enemy to play with.