AI - part 5

Top  Previous  Next

Welcome to the AI workshop! It's time for the 5th episode in the series, which, as promised, adds "a (mildly) furious enemy chasing the player". Well, I thought that even a mildly furious enemy might scare some of you, so I thought that I'd add a friendly rabbit that tries to find his master (the player) instead - see for yourself!

 

aum110_ai1

 

Do I get an awww for that? I don't? Well... I thought I won't, but it was worth the try.

 

Joking aside, this demo is the first one that showcases the actual power of the path finding algorithm. In fact, you could simply replace the rabbit model with a more manly looking model, add in some quick weapon code and you'd be on your way to building a real-life, fierce AI machine. I want us to do it all properly, so we're not there quite yet, but if you are the type of person that likes to play with code (read "a real programmer") you could take what we have here and develop it without having to go through too much trouble.

 

The core parts of the path finding code didn't change; we will discuss the functions that were edited, as well as the new additions below. On a side note, I have kept the path generation / display method using the mouse button clicks for now, so you can still test and see the actual paths from the current player location to the clicked node.

 

function trace_back()

{

       if (event_type == EVENT_SCAN)

       {

               my.skill45 = handle(you); // store "you" because trace will destroy it

               if (you.skill47 == 1234) // scanned by a node

               {

                       trace_me();                

               }

               else // scanned by the rabbit (skill47 = 5678)

               {

                       trace_him();

               }

       }

       if (event_type == EVENT_TOUCH) // node touched with the mouse?

       {

               touched_node = my.skill48; // then get the number of the node that was touched with the mouse pointer

       }

       if (event_type == EVENT_CLICK) // clicked with the mouse?

       {

               start_node = my.skill48; // then this node will become start_node

       }

}

 

Function trace_back( ) was edited, allowing the enemies to scan the nodes as well. The nodes only scanned each other until now, trying to determine which nodes can see each other directly (using c_trace). The modified function allows the enemies to scan the nodes as well, with the purpose of getting the closest node to the enemy (the rabbit, in this demo) that can actually see the enemy.

 

Function trace_him( ) is a new addition, being the one that helps the enemy detect the closest node to it, and then feeding the value that will be stored in start_node to the path finding function.

 

function trace_him()

{

       wait (1); // avoid the error message dangers

       if (c_trace (my.x, you.x, IGNORE_ME | IGNORE_MODELS | IGNORE_PASSENTS) == 0) // if this node can "see" the rabbit that scanned it

       {

               you = ptr_for_handle(my.skill45); // restore the "you" pointer

               dist_to_rabbit[my.skill48] = vec_dist (my.x, you.x);

               if ((dist_to_rabbit[my.skill48] < min_rabbit) && (dist_to_rabbit[my.skill48] != 0))

               {

                       min_rabbit = dist_to_rabbit[my.skill48];

               }

               wait (2); // allow the rest of the nodes to change min_rabbit (if they should change it)

               if (dist_to_rabbit[my.skill48] == min_rabbit) // this is the closest node to the rabbit?

               {

                       start_node = my.skill48;

               }

       }

       else

       {

               dist_to_rabbit[my.skill48] = 0;

       }                                

}

 

Basically, we are tracing from the node to the rabbit; if it is visible, we compare the distance to it with the distance from the other nodes that can see the rabbit to the rabbit. If this is the minimum distance, we set start_node to the number of that particular node.

 

OK, so now we know the enemy starting node - what about the target node? Take a look at the function below to see how that works.

 

function target_startup()

{

       while (1)        

       {

               index %= max_nodes; // limit "index" to 299 in this demo

               if ((node_to_player[index] < closest_distance) && (see_player[index] == 1))

               // if the distance from this node to the player is smaller than the current distance to the player

               // and we are dealing with a node that can see the player

               {

                       closest_distance = node_to_player[index]; // then we have a new winner!

                       target_node = index; // and the target is given by the current "index" value

               }

               else // haven't found a new winner?

               {

                       closest_distance = node_to_player[target_node]; // then use the previous winner

               }

               index += 1;

               wait (1);

       }                

}

 

It's a startup function, one that will run at game start, and its role is to compare the distances from all the nodes to the player once per frame. Actually, we could even do this once or twice per second, and it would still be accurate enough. If we have found a node which has a minimum distance to the player, that's our target note, so with start_node and target_node set to the proper values, the path finding function has all it needs to run.

 

Ready to take a look at the actual rabbit / enemy code?

 

action my_rabbit()

{

       var anim_percentage;

       VECTOR temp, marker_pos;

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

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

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

 

The code waits until all the path / node distances are computed; it could take a second or two until it's all done, because we're running about 50 c_scan instructions one frame away from the other, as well as quite a few c_trace instructions, but after that we're ready to go and we won't have to wait again.

 

       while (player)

       {

               my.lightrange = 400; // emit light

               my.red = 255; // red light

               my.green = 0;

               my.blue = 0;

 

The rabbit generates a red light and the player has got a blue light; this way, they're much easier to distinguish, taking into account the fact that our playground is a huge level.

 

aum110_ai2

 

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

               {

                       anim_percentage += 2 * time_step; // 2 = idle (stand) animation speed

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

                       wait (1);

               }

 

If the rabbit can see the player, it must have reached the destination node, so we play its idle / stand animation. The rabbit will spend most of its time trying to find the player, though, and the "else" branch below takes care of that.

 

               else

               {

                       min_rabbit = 999999; // set the minimum distance from the rabbit 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;

                       }

 

The enemy loop will run several times, so we need to set min_rabbit to a huge value. It's the variable that keeps track of the distance between the closest node that can see the rabbit and the rabbit model. Since that value might change as the rabbit moves towards a new node, we need to set it to a huge value each time The Path of the Rabbit (sounds like an interesting movie title!) is computed. Then, we scan around the enemy, trying to determine the closest node. Finally, we reset the array that was storing a previous path.

 

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

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

                       snd_play (getpath_wav, 70, 0);

                       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 first element of the path that leads our rabbit to its master (that would be you) is the node that's closest to the rabbit right now, so we set it as being the fist path element. We play a "ready to find a new path" sound, and then, if start_node and target_node aren't the same (the rabbit didn't reach the target), we run our find_path function, which will compute the shortest path between start and target, storing something like 2, 21, 5, 18, 33, 7, etc in the path array.

 

In the example above, the first node on the proper path would be 2, the 2nd one would be 21, and so on. As you know, these numbers are automatically assigned at game start to each node, so there's no need to worry about them. The last line of code in the snippet above simply copies the number of nodes on the computed path (6 if we use the same example).

 

                               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 rabbit's height for z

                                       d1_marker = ent_create (marker_mdl, marker_pos, follow_path); // create an invisible entity that guides the rabbit

                                       wait (1);

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

                                       {

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

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

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

                                               wait (1);

                                       }

                                       d1_marker = NULL;

                                       i -= 1;

                                       wait (1);

                               }

                       }        

               }

       }

}

 

OK, so now our rabbit knows what path he should follow, but how do we convert those array values into something that can actually move our rabbit from the starting point to the target? We will use a while loop that runs for as long as we didn't visit all the points on the computed path. We extract the node x and y values for each node on the proper path (we'll use rabbit's initial z as well), storing them inside the marker_pos vector, and then we create an invisible entity, assigning it the d1_marker name - this will be the target for our rabbit.

 

The inner while loop moves the rabbit towards the target node until its distance to the node is smaller than 10 quants; when this happens, the d1_marker entity is made NULL, so that we can create a new target that leads to the 2nd node on the path, and so on.

 

The last function drives that invisible entity that serves as a target for the rabbit; its main goal is to turn the rabbit towards the path marker each frame, thus helping it prevent target misses because of wall glides, etc.

 

function follow_path()

{

       VECTOR temp;

       set (my, INVISIBLE | PASSABLE);

       while (me)

       {

               vec_set (temp.x, my.x);

               vec_sub (temp.x, you.x);

               vec_to_angle (you.pan, temp);

               wait (1);

       }

}

 

So now we have an actual enemy running on a properly computed path, chasing the player. I'm pretty excited when I'm thinking at what's coming up in this workshop series, so I hope to see you all soon!