AI - part 3

Top  Previous  Next

Welcome to the third episode in the AI series! This month we will move on to much more advanced AI stuff, starting to put together a node-based AI system which will work flawlessly - at least that's what I hope it will happen :)

 

Some of you might remember my C-script based "Perfect AI" code, which was renamed later on to a much more modest "Purrfect AI". That code was pretty advanced for its time, considering the fact that the engine was lacking several important features, like multidimensional arrays, for example. This has forced me to bind together simple arrays, in order to replace the missing multidimensional arrays, and so on. Fortunately, everything has changed for the better during the past few years, and Gamestudio's engine not only supports pretty much everything that's needed for AI programming, but is also much faster.

 

Let's open and run the ai7.c script; you should see something that looks like this:

 

aum108_ai1

 

The test level is very similar with the old Purrfect AI demo level; actually, I have tried to reuse that level, but it didn't work, so I had to recreate it - the old one was built 10 years ago! Anyway, you will see that we have a player model that can be moved using the arrow keys and various nodes that can have a red, blue or green color.

 

Use the arrow keys to move the player around; I'm not using any collision detection in this demo, so feel free to pass through the walls in order to reach any area of the level as quickly as possible. As you move the player, you will notice that some of the nodes change their colors; here's an example:

 

aum108_ai2

 

Take a look at the yellow digits that can be observed at the top of the screen: the first one shows the number of the target node, the node that is the closest to the player, and "D. to target" shows the distance to the target, the distance (in quants) between the closest node and the player.

 

What about the node colors? The green ones are close enough to the player and can see the player, the blue ones are close enough to the player but can't see the player and the red ones are away from the player, so they don't use precious CPU resources. This is the foundation of our advanced AI system: we place several nodes in the level, and then they start tracking the player, creating the best, shortest path that would lead the enemy towards it each and every time.

 

If this has gotten you excited, then it is definitely the time to take a good look at the code.

 

#include <acknex.h>

#include <default.c>

 

STRING* ai7_wmb = "ai7.wmb";

 

var player_speed; // player's movement speed

 

var node_number = 0;

 

var node_distance[50];

var see_player[50];

var node_coords[50][2];

 

var node_sensitivity = 500; // nodes placed more than 500 quants away from the player will be ignored

var target_node = 0; // closest node to the player that can "see" the player

var closest_distance = 9999; // will hold the distance from the closest visible node to the player

var index = 0; // used as a counter for all the nodes

 

FONT* ai_font = "arial14.pcx";

 

We begin with a few standard definitions; the node_number variable will store the total number of node (I have used a total of 30 nodes for this demo, but sky's the limit).The node_distance[50] array stores the distance from each node to the player; it can store distances for up to 50 nodes at the moment, but you can easily increase that limit by playing with 50. The same thing goes for the next two arrays; see_player[50] can have values of 1 (the node can see the player) or zero (the node can't see the player). Finally, node_coords[50][2] stores the x and y coordinates for up to 50 nodes. Don't forget that you can easily increase these limits by replacing 50 with 500, for example.

 

The following variable sets the node sensitivity; by default, any node that is placed more than 500 quants away from the player will be ignored, in order to save precious system resources. Actually, if you plan to run the AI code on old PCs, you should definitely take into account the idea of reducing this value to 300, or even less, depending on the configuration of your levels.

 

The rest of the vars are common, excepting the closest_distance variable, which stores the distance between the closest visible node and the player. Its initial value is 9999 (any huge value will do here), but it will be set to a real life value as soon as the first node is loaded.

 

function main( )

{

       video_mode = 8; // 1024x768;

       fps_max = 60; // limit the frame rate to 60 fps

       level_load(ai7_wmb);

       wait (3); // wait for the level to be loaded

       camera.pan = 90;

       camera.x = 0;

       camera.y = 0;

       camera.z = 2800; // choose a convenient camera height, play with this value

       camera.tilt = -90; // and make it look down

       while (1)

       {

               index %= 49; // limit the "index" value

               index += 1; // from 1 to 49

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

               {

                       closest_distance = node_distance[index]; // 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_distance[target_node]; // then use the previous winner

               }

               wait (1);

       }                

}

 

Function main does a few standard things, like setting the video resolution, the frame rate, the camera position and the like. Nevertheless, the while (1) loop plays an important part: it goes through all the nodes, comparing their distance to the player and finding the closest one that can see the player. Basically, this loop finds the closest node to the player that can actually see the player.

 

action player1( ) // simple player movement action, doesn't use collision detection

{

       player = me; // I'm the player

       while (1)

       {

               my.x += 10 * (key_cur - key_cul) * time_step;

               my.y += 10 * (key_cuu - key_cud) * time_step;                

               wait (1);

       }

}

 

Player's action is as simple as it can be; it changes the x and y coordinates of the model with a speed given by 10 * time_step.

 

action node() // using 30 nodes in this demo

{

       my.skill47 = 1234;

       set (my, PASSABLE);

       my.skin = 1;

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

       node_number += 1;

       my.skill48 = node_number;

       node_coords[my.skill48][0] = my.x;

       node_coords[my.skill48][1] = my.y;

 

Finally, the action that's attached to the nodes does all the heavy work. First of all, it sets a unique value for skill47, allowing us to identify it (might be a useful feature to have for later). All the nodes are passable; their default skin value is 1 (red). Each node will wait until the player model is loaded in the level, and then it will get a unique ID number, storing it inside its own skill48. The following two lines store the x and y position of the node inside the array; they will be used to find out which node is the closest to the player.

 

       while (1)

       {

               node_distance[my.skill48] = vec_dist(player.x, my.x);

               if (node_distance[my.skill48] < node_sensitivity)

               {

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

                       {

                               my.skin = 2;

                               see_player[my.skill48] = 1;

                       }

                       else // the node is close to the player, but it can't see the player

                       {

                               see_player[my.skill48] = 0;

                               my.skin = 3; // then let's keep the blue color for its skin

                       }

               }

               else // the node is far from the player? Then let's keep the red color

               {

                       my.skin = 1;

               }

               wait (-0.1); // trace 10 times a second, want to conserve precious system resources

       }

}

 

The while loop computes the distance between the node and the player; if this distance is smaller than the node sensitivity value (500 quants by default) and the node can see the player (we use a c_trace instruction to check that), the node changes its skin color to green (skin = 2) and the corresponding array value is set to 1.

 

If the node is close enough to the player, but it can't see the player, the value inside the see_player array is set to zero and the node changes its skin color to blue (skin = 3). Finally, if the node is away from the player, it will keep its initial skin color (red, skin = 1).

 

All these operations are done 10 times per second; we don't need to perform them faster and we're saving some precious CPU resources that way.

 

That's all for now, folks! Next month we are going to add the actual path finding code to the mix, so we're going to have an exciting AI article. I'll see you all soon!