Surprise workshop

Top  Previous  Next

Welcome to another AI workshop episode. It's one that has incorporated a lot of work, but I think it was well worth the effort.

 

First of all, we have gotten ourselves a level filled with enemies. Actually, there are only three of them, but due to their skills, they can cover the level quite well. Don't worry, I have left a few areas without nodes, allowing the player to rest there for a while.

 

aum115_workshop1

 

These guys can climb and descend stairs and (another new feature!) are able to navigate away from their paths if it's convenient to do so - if they can shoot the player, for example.

 

aum115_workshop2

 

Another new feature will get the enemies out of trouble if they get stuck somewhere in the level due to poor node placement (nodes that can see each other but shouldn't, for example) or if they move too far away from the nodes that show them the paths. Or if they get stuck because of a fellow enemy! Believe it or not, these guys will even jump on top of each other, in their desire to bring the player down as quickly as possible! I've seen it happening, and it was quite scary...

 

Finally, I have fixed a few more minor bugs. But I won't discuss them here. One of them was quite embarrassing; the player was able to shoot even after dieing :)

 

So let us focus our attention on the enemy code, where all the fun stuff happens.

 

action my_enemy()

{

       var anim_percentage;

       var shoot_percentage;

       var distance_covered;

       VECTOR temp, marker_pos, bullet_coords;

       ANGLE temp_angle;

       while (!player) {wait (1);} // wait until the player is created

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

 

The enemy waits until the player model is loaded, and then waits until the distances between the path finding nodes are computed. It's a process that only needs to run once, when the level is loaded.

 

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

       enemy_id += 1; // get a unique id number (0..3)

       my.skill48 = enemy_id; // and store it in skill48

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

 

We can determine if an entity is an enemy or not because we set its skill47 to 5678 (just a random value). This way, when we want to determine if the player has fired at an enemy, we can check if you.skill47 = 5678, for example. In addition to this, each enemy has a particular id, which is stored inside its own skill48. Finally, the health of the enemies is stored in their own skill88.

 

       while (player)

       {

               while (vec_dist(player.x, my.x) > 2000) // animate the enemy using its "stand" animation while the player is away from it

               {

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

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

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

                       wait (1);

               }

 

Most of the action takes place inside the while loop above. First of all, the enemy checks if it is closer than 2000 quants to the player or not. If the distance is bigger than 2000 quants, the enemy plays its "stand" animation. Unless it has been shot by the player from a huge distance, so it has to die, getting out of the "stand" animation loop because of the "break" instruction.

 

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

               {

                       vec_for_vertex(bullet_coords, my, 31); // get the coordinates of an outside gun vertex

                       if (c_trace (bullet_coords.x, player.x, IGNORE_ME | IGNORE_MODELS | IGNORE_PASSENTS) == 0) // if the enemy's weapon can "view" the player

                       {

                               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;

 

If the enemy can see the player, it tries to determine if its firing vertex can see the player as well. And if this happens, the enemy rotates towards the player, while keeping its tilt angle intact. Enemies with non-zero tilt angles look terrible imho, but if you want to see how they look that way, you can comment the last line of code above.

 

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

                               {                        

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

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

                                           if(anim_percentage % 100 < 3) //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);

                                               }                                        

                                       }

                               }

                       }

 

If the player is still alive, the enemy will fire at it using its 7th vertex as an origin for the bullets. That is the best value for this particular model, so feel free to change it with one that's perfect for your enemy models. Finally, the bullet_mdl entity is created at the position given by bullet_coords and the function named move_bullets is assigned to it. A bullet sound is played as well.

 

                       else

                       {

                               // move the enemy sideways until its weapon can see the player

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

                       }

               }

 

The "else" branch above is run when the enemy can see the player, but it can't get a clear shot at it (the weapon tip doesn't "see" the player). If this is the case, the enemy will move sideways until its gun can successfully fire at the player.

 

               else // the enemy can't see the player, so it will compute the path that leads to it

               {

                       min_enemy = 999999; // set the minimum distance from the enemy to its surrounding nodes to a huge value

                       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

                       i = 0;

                       while (i < max_nodes)

                       {

                               enemy_path[i][my.skill48] = 0; // reset the "local" path storing array for each enemy

                               i += 1;

                       }

 

The "else" branch above is run when the enemy can't see the player, but it is close enough to it. Under these circumstances, the enemy will ask the computer to generate the shortest path towards the player. Everything begins with a scan in the area, looking for the nodes that could provide guidance. Then, the master (global) array that stores the paths is reset. Finally, the "local" array is reset - this array will store this enemy's particular path.

 

                       if (start_node != target_node) // the enemy hasn't arrived at the destination yet?

                       {

                               while (pathfinder_available == 0) // this loop will run while the enemy is waiting for a new path

                               {

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

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

                                       anim_percentage %= 100;

                                       wait (1);

                               }

                               pathfinder_available = 0; // the path finding function is now busy

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

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

                               i = 0;

                               while (i < max_nodes)

                               {

                                       enemy_path[i][my.skill48] = path[i]; // copy the path that was stored in the "global" array to enemy's "local" path array

                                       i += 1;

                               }

                               wait (1);

                               pathfinder_available = 1; // the path finding function can be used again

 

If the enemy hasn't arrived at the destination yet (start_node and target_node are different), we wait until the path finding function is available. We only use a function because we don't want to waste precious CPU resources, and it's only going to be needed once every few seconds (or so). The enemies will play their stand animations while waiting for their paths, but the path finding code runs so fast that I've never see that happening.

 

Function find_path will determine the shortest path, storing it in the global "path" array. Then, the path is copied locally, to the enemy's "local" enemy_path array. Finally, the path finding function is made available to other enemies that may need it.

 

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

                               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

                                       // this value should be set depending on the distance between player's z and the height of the nodes. Better use a bigger value instead of using a small one

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

                                       {

                                               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;

 

It's time to move the enemy towards each node on the path, one node at a time. We use a local vector (marker_pos) to store the coordinates of the next node on the shortest path to the player, and then we rotate the enemy towards the node, setting the enemy's tilt to zero once again (for obvious reasons :)

                                               

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

                                               vec_set (temp.x, my.x);

                                             temp.z -= 1000; // apply gravity forces for our enemy as well

                                               // 23 allows the enemy to climb stairs, so play with it (as well as with the origin of the enemy model) if it doesn't look natural

                                             temp.z = -c_trace (my.x, temp.x, IGNORE_ME | IGNORE_PASSABLE | USE_BOX) + 23;

                                             temp.x = 10 * time_step; // this value sets the enemy's movement speed

                                             temp.y = 0;

 

Each time we use a loop, we need to make sure that the enemy can die in an instant, and not after it has reached the next point on the path, for example. That's why we are constantly checking if the enemy's skill88 (health) is greater than zero, and we break out of the while loop if it isn't.

 

Our enemies use gravity as well; they can climb and descent stairs, and "23" sets the maximum step height that can be climbed. Play with this value until you get it right for your level and enemy models. One more thing: the enemy's movement speed is given by 10 * time_step.

 

                                               // store the distance covered each frame in the distance_covered variable

                                               distance_covered = c_move (my, temp, nullvector, IGNORE_PASSABLE | IGNORE_FLAG2 | GLIDE); // 10 = movement speed

                                               if (distance_covered < 0.2) // enemy got stuck while navigating towards the following node on the path (perhaps by running into another enemy, poor node placement, etc)

                                                       break; // then get out of the movement loop and request a new path!

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

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

 

As you may remember, we have given the enemy much more freedom in this script; it can move away from nodes to get a better shot at the player, for example, so it may get stuck in a wall every now and then. So how do you fix a problem like this? Here's what I have done: I am evaluating the distance covered by the enemy each frame, using a local variable named... distance_covered, what else?

 

If the enemy gets stuck in a wall, for example, its distance_covered value gets close to zero, so the code tells the enemy to break out of this loop, generating a new path towards the player. The last two lines of code set the enemy's "run" animation speed and play it.

 

                                               // 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);        

       }

 

Older, dumber enemies were only requesting a new path after reaching their destinations, but the new enemies are constantly trying to hunt down the player. The code is using c_scan to track the enemy, and if the player is visible, the old path is dumped and a new one is requested. This way, the enemy will constantly adjust its trajectory, tracking the player at all times.

 

       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);

}

 

The last few lines of code are run when the enemy dies. We play a sound, we play the enemy's "death" animation, and then we make its corpse passable. How simple is that?

 

I hope that you have learned at least a few things from this workshop. And I also hope to see you all soon!