Perfect AI - part III

This month we will witness player's panic as he (or she) is being attacked by a drone that uses the Perfect AI code we've covered so far. We will discuss many new things but don't worry, the hard part is over.

Your assignment is simple: run for your life all the time. If the drone runs into you, it kills you instantly. If the drone sees you, it starts firing rockets at you; every rocket takes 10 points from your health. The drone will chase you to your death but you've got a bigger movement speed so you should be able to stay alive for a while :)

This Perfect AI series has started two months ago; therefore, I encourage you to read the articles in Aum27 and Aum28 before moving on. This article will describe only the functions and actions that were added or were changed. The script file - ai3.wdl - is split in two parts; the new code added for this month's demo is placed below these lines in ai3.wdl:

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// new stuff for the 3rd Perfect AI episode
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Let's examine the new code.

var dist_to_drone[30];
var drone_start[12] = 830, 700, 100, -830, 700, 100, 830, -700, 100, -830, -700, 100;

The first array holds the distance from the drone to its surrounding nodes (or zero if the nodes can't see it). Drone_start holds the starting coordinates for the drone model.

I chose four starting positions for the drone and drone_start[12] holds their coordinates. The drone will pick a random starting point every time.

text ai_txt // displays the texts on the screen
{
font = system_font;
pos_x = 0;
pos_y = 0;
string = "Current path: \n\nHealth:\nTarget:\nStart:";
flags = visible;
}

panel ai_pan // displays the numerical values on the screen
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 80, 24, 2, system_font, 1, player.health;
digits = 80, 37, 2, system_font, 1, target_node;
digits = 80, 50, 2, system_font, 1, start_node;
digits = 120, 0, 4, system_font, 1, path[0];
digits = 150, 0, 4, system_font, 1, path[1];
digits = 180, 0, 4, system_font, 1, path[2];
digits = 210, 0, 4, system_font, 1, path[3];
digits = 240, 0, 4, system_font, 1, path[4];
digits = 270, 0, 4, system_font, 1, path[5];
digits = 300, 0, 4, system_font, 1, path[6];
digits = 330, 0, 4, system_font, 1, path[7];
digits = 360, 0, 4, system_font, 1, path[8];
digits = 390, 0, 4, system_font, 1, path[9];
digits = 420, 0, 4, system_font, 1, path[10];
digits = 450, 0, 4, system_font, 1, path[11];
digits = 480, 0, 4, system_font, 1, path[12];
digits = 510, 0, 4, system_font, 1, path[13];
digits = 540, 0, 4, system_font, 1, path[14];
flags = overlay, refresh, visible;
}

We are using a text and a panel to display the current path from the drone to the player, start_node, target_node and player's health.

The action associated to the nodes has changed a little so we will discuss it again briefly:

action node
{
my.invisible = on;
my.enable_scan = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;
number_of_nodes += 1;
my.skill48 = number_of_nodes;
nodex[my.skill48] = my.x; // store x and y
nodey[my.skill48] = my.y; // for this node
while (number_of_nodes < (max_nodes - 1)) {wait (1);}

I chose to hide the nodes in this demo but you can make them visible again by commenting the first line. The nodes are sensitive to scanning; if they are scanned, function trace_back() will run. We set a weird value for skill47 (1234) to make sure that we can detect if a certain entity is a node or not and we make the nodes passable because they shouldn't block the player or the drone. Every node gets a unique ID number (0...29) which is store in its skill48. We also store the x and y coordinates for every node in nodex and nodey. Finally, we wait for all the nodes to be placed in the level.

while (current_node <= number_of_nodes)
{
if (current_node == my.skill48)
{
temp.x = 360; // horizontal scanning angle
temp.y = 30; // vertical scanning angle
temp.z = 1000; // scanning range
scan_entity (my.x, temp); // scan the nodes nearby
wait (1);
current_node += 1; // move to the next node
}
wait (1);
}
distances_computed = 1;

Every node will scan the nodes that are closer to 1000 quants to it; when all the nodes have scanned their neighbors the var named distances_computed is set to 1.

while (player == null) {wait (1);}
while (1)
{
node_to_player[my.skill48] = vec_dist(player.x, my.x);
if (node_to_player[my.skill48] < 500)
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
see_player[my.skill48] = 1;
}
else
{
see_player[my.skill48] = 0;
}
}
sleep (0.1);
}
}

We wait until the player is created and then we store the distance from every node to the player inside the array named node_to_player. If the player comes closer than 500 quants to a node we trace from the node to the player; if the node can "see" the player, it sets see_player to 1, otherwise see_player will be set to 0. Every node traces 10 times a second.

Time to take a look at the event function associated to the nodes:

function trace_back()
{
if (event_type == event_scan)
{
if (you.skill47 == 1234) // scanned by a node
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45); // restore the "you" pointer
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}
else // scanned by the drone (skill47 = 5678)
{
my.skill45 = handle(you);
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0)
{
you = ptr_for_handle(my.skill45);
dist_to_drone[my.skill48] = vec_dist (my.x, you.x);
if ((dist_to_drone[my.skill48] < min_drone) && (dist_to_drone[my.skill48] != 0))
{
min_drone = dist_to_drone[my.skill48];
}
wait (2);
if (dist_to_drone[my.skill48] == min_drone) // this is the closest node to the drone?
{
start_node = my.skill48;
}
}
else
{
dist_to_drone[my.skill48] = 0;
}
}
}
}

If the node is scanned by another entity, there are two possibilities:
1) The node was scanned by another node (skill47 = 1234). If this is true, we store the pointer to the entity that has performed the scanning (you) and then we trace from this node to "you". If the node can see the entity that has scanned it, we store the distance between the node and the entity in node_to_node;
2) The node was scanned by the drone (skill47 = 5678). We store the "you" pointer and then we trace back from the node to the drone. If this node can see the drone, we store the distance between the drone and the node in dist_to_drone. The variable named min_drone will store the distance from the closest node to the drone; start_node will be the closest node to the drone if its dist_to_drone will hold a value equal to min_drone.

You can see that the drone has scanned 5 nodes but only node 2 and node 4 can see the drone; start_mode will be set to 2 because that's the closest node to the drone that can see the drone.

The code associated to the player has changed too:

action player1
{
player = me;
my.health = 100;
my.enable_entity = on;
my.enable_impact = on;
my.event = decrease_health; // I loose health when I collide with entities

The player has 100 health points (health = skill40) and it is sensitive to other entities (the drone and its rockets). The event associated to player's action is decrease_health.

while (my.health > 0)
{
my.pan += 4 * (key_cul - key_cur) * time;
my.skill1 = 15 * (key_cuu - key_cud) * time;
if (key_cuu + key_cud > 0)
{
ent_cycle("walk", my.skill46); // play "walk" frames animation
my.skill46 += 10 * time; // "walk" animation speed
my.skill46 %= 100; // loop animation
}
else // the player is standing
{
ent_cycle("idle", my.skill46); // play "stand" frames animation
my.skill46 += 2 * time; // "stand" animation speed
my.skill46 %= 100; // loop animation
}
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}

As long as the player is alive, it can move and rotate using the arrow keys. If the player is walking, it plays its "walk" animation; otherwise, it plays its "stand" animation. The player moves ignoring the passable entities (the nodes) and glides along the walls if it collides with them.

my.skill46 = 0;
while (my.skill46 < 80)
{
ent_cycle("death", my.skill46); // play "death" animation
my.skill46 += 2 * time;
wait (1);
}
}

When the player is dead, it plays its "death" animation. Let's take a look at the code associated to the drone now:

starter drone
{
while (total_frames < 5) {wait (1);}
ent_create (drone_mdl, nullvector, init_drone);
}

The drone is created by a "starter" function that waits for 5 frames and then creates the drone in the level and associates it the function init_drone:

function init_drone()
{
while (player == null) {wait (1);}
var corner;
my.skill47 = 5678;
corner = sys_seconds % 4;
if (corner == 0)
{
my.x = drone_start[0];
my.y = drone_start[1];
my.z = drone_start[2];
}
if (corner == 1)
{
my.x = drone_start[3];
my.y = drone_start[4];
my.z = drone_start[5];
}
if (corner == 2)
{
my.x = drone_start[6];
my.y = drone_start[7];
my.z = drone_start[8];
}
if (corner == 3)
{
my.x = drone_start[9];
my.y = drone_start[10];
my.z = drone_start[11];
}

The function waits until the player is created and then creates a local variable named corner. We set skill47 = 5678 as a unique ID for the drone and then we generate a random number from 0 to 3 using sys_seconds. We choose one of the four starting positions for the drone depending on the random value stored in "corner".

while (player.health > 0)
{
my.lightrange = 400; // emit light
my.lightred = 0;
my.lightgreen = 255; // green light
my.lightblue = 0;
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
shoot_rocket();
sleep (1);
}

As long as the player is alive, the drone emits green light on a diameter of 400 quants. If the drone can see the player (trace = 0) it starts shooting rockets at it every second.

else
{
min_drone = 999999;
temp.x = 360; // horizontal scanning angle
temp.y = 360; // vertical scanning angle
temp.z = 800; // scanning range
scan_entity (my.x, temp);
sleep (1);

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 the drone can't see the player, min_drone is set to a huge distance and the drone starts scanning on a range of 800 quants in order to get the closest node to it. We reset the array named "path" (that's the array that stores the current path) and then we write the first path element = target_node.

snd_play (getpath_wav, 70, 0);
if (start_node != target_node)
{
find_path(start_node, target_node);
wait (2);
i = k;
while (i >= 0)
{
temp.x = nodex[path[i]];
temp.y = nodey[path[i]];
temp.z = my.z; // use drone's height for z
d1_marker = ent_create (marker_mdl, temp, follow_path); // create an invisible entity that guides the drone
wait (1);
while (vec_dist (d1_marker.x, my.x) > 10)
{
my.skill1 = 10 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}
i -= 1;
wait (1);
}
}
}
}
}
We play a sound that announces a new path search and then, if start_node and target_node are different, we run the find_path function from Aum28; this recursive function finds the shortest path between the two nodes passes as its parameters. The result of the function is stored in the array named path; we've talked about it in the previous paragraph.

We wait for 2 frames and then we start moving the drone from one path point to another. We create an entity named d1_marker above every node along the shortest path and then we move the drone towards it until the distance between the drone and the invisible d1_marker entity is below 10 quants; when this happens, we create d1_marker above the next node on the shortest path and then we move the drone towards it. The process repeats until the drone has reached the last node stored inside the array named path.

The entity named d1_marker runs this simple function:

function follow_path()
{
my.invisible = on;
my.passable = on;
vec_set (temp.x, my.x);
vec_sub (temp.x, you.x);
vec_to_angle (you.pan, temp);
while (d1_marker == me) {wait (1);}
ent_remove (me);
}

The function makes the marker invisible and passable and rotates the drone towards it. The older unused markers will be removed as new markers are being created.

Let's take a look at the functions that create the rockets:

function shoot_rocket()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp); // turn the drone towards the player
if (player.health > 0)
{
ent_create (rocket_mdl, my.pos, move_rocket); // create a rocket
}
}

Let me remind you that the function named shoot_rocket() runs when the drone can see the player. The function turns the drone towards the player and if the player isn't dead it creates rockets and runs them using the function below:

function move_rocket()
{
my.pan = you.pan;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_rocket;
while (my != null)
{
my.skill1 = 15 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable; // ignores the drone (its creator = you)
ent_move(my.skill1, nullvector);
wait (1);
}
}

The rockets and their parent (the drone) have the same pan angle and they are sensitive to impact with other entities or with the level blocks. The rocket moves with the speed given by its skill1 and it ignores its creator, the drone (you) while moving. If the rocked collides with something, its remove_rocket() event will run:

function remove_rocket()
{
snd_play (hit_wav, 100, 0);
wait (1);
ent_remove (me);
}

That's an easy function so I'll let you figure out what it does. I'm going to take care of the last function:

function decrease_health()
{
if (you.skill47 == 5678) // collided with the drone?
{
my.health = 0; // instant death
}
else // hit by a rocket?
{
my.health -= 10; // loose 10 health points
}
if (my.health <= 0)
{
my.event = null;
snd_play (death_wav, 40, 0);
}
}

This function is the event function associated to the player. If the player collides with the drone (skill47 = 5678) the player will die instantly; otherwise, the player was hit by a rocket and it will loose 10 health points. If its health decreases to 0, the player will stop responding to events and it will play its death sound.

That was the first playable demo that uses the Perfect AI core. The drone will search for the player and will find it every time; the only drawback is that the while loop keeps running until all the points on the path are visited so if the player changes its position quickly the drone won't be able to detect its new location until it reaches the last point of the previous path. I have coded the drone this way because you can see and understand how the paths are changing from one target_node to another; however, this limitation can be overcome easily by inserting a new trace in the while loop that moves the drone.

Next month we will get out of the maze level (sniff...) and we will move into a typical 3D shooter level with human enemies. I promise to give the player a gun so stay tuned!

Oh, and don't blame me if the player model penetrates the walls during its "death" animation sequence:

This problem appears because of the strange "death" animation of the model and has nothing to do with my code.

Lazer eyes

This article will teach you how to create particle rays and how to attach them to the eyes of your models.

I know I wouldn't want to meet this guy on a dark alley... I'm talking about the red little guy, of course :)

The model plays its "stand" animation and these two laser rays that come from its eyes move continuously as it rotates the head.

action night_monster
{
while (1)
{
ray_size = 2;
ray_width = 10;
ent_cycle("stand", my.skill1); // play "stand" frames animation
my.skill1 += 0.4 * time; // animation speed
my.skill1 %= 100; // loop animation
vec_for_vertex (left_eye, my, 235); // get the vertex coords for the left eye
vec_for_vertex (right_eye, my, 247); // get the vertex coords for the right eye
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
while (ray_size > 0)
{
vec_for_normal (temp_left, my, 235);
vec_for_normal (temp_right, my, 247);
vec_normalize (temp_left, ray_size);
vec_normalize (temp_right, ray_size);
vec_add (left_eye, temp_left);
vec_add (right_eye, temp_right);
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
ray_size -= 0.01;
}
wait (1);
}
}

The model plays its "stand" animation in a loop. We get the vertex coordinates for the left eye (vertex #235) in left_eye and we store the coordinates for the right eye (vertex #247) in right_eye. We create two flares using left_eye and right_eye as their starting points. Those of you that don't need more than a pair of eyes glowing in the dark should comment the following while loop.

action night_monster
{
while (1)
{
ray_size = 2;
ray_width = 10;
ent_cycle("stand", my.skill1); // play "stand" frames animation
my.skill1 += 0.4 * time; // animation speed
my.skill1 %= 100; // loop animation
vec_for_vertex (left_eye, my, 235); // get the vertex coords for the left eye
vec_for_vertex (right_eye, my, 247); // get the vertex coords for the right eye
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
// while (ray_size > 0)
// {
// vec_for_normal (temp_left, my, 235);
// vec_for_normal (temp_right, my, 247);
// vec_normalize (temp_left, ray_size);
// vec_normalize (temp_right, ray_size);
// vec_add (left_eye, temp_left);
// vec_add (right_eye, temp_right);
// effect (attach_rays, 1, left_eye, normal);
// effect (attach_rays, 1, right_eye, normal);
// ray_size -= 0.01;
// }
wait (1);
}
}

And now, for the rest of us that want to use the full effect let's take another look at the while loop:

while (ray_size > 0)
{
vec_for_normal (temp_left, my, 235);
vec_for_normal (temp_right, my, 247);
vec_normalize (temp_left, ray_size);
vec_normalize (temp_right, ray_size);
vec_add (left_eye, temp_left);
vec_add (right_eye, temp_right);
effect (attach_rays, 1, left_eye, normal);
effect (attach_rays, 1, right_eye, normal);
ray_size -= 0.01;
}

We get the resulting normals for the faces that surround the left and the right eye, we set their length to ray_size and then we add them to the original left_eye and right_eye, generating particles for as long as ray_size is above 0. Let's see a picture that is explains what's happening:

I have used fewer particles in this example to make the things easier for you; the first particle is created when ray_size = 2, the second particle after the first subtraction inside the while loop (ray_size = 1.99) and so on. The process stops when ray_size = 0.

And now let's see the functions associated to the particles:

function attach_rays()
{
my.bmap = flare_pcx;
my.flare = on;
my.bright = on;
my.size = ray_width;
my.function = remove_flares;
ray_width -= 0.01;
}

function remove_flares()
{
my.lifespan = 0; // remove the flare particle
}

The particles live for a single frame; the only thing that needs to be mentioned here is that the size of the particles is variable; the first particles (the ones that are away from the eyes) are the biggest ones while the later particles (placed close to the eye) get smaller and smaller. This effect is achieved by decreasing ray_width with 0.01 for every particle that runs the function. Ray_width is the variable that controls the size of the particles so that's all we need to do in order to achieve the effect below.

I encourage you to play with the initial values for ray_size, ray_width and to change the bitmap if you want to create a plethora of interesting effects. Don't forget to edit the numerical value inside the while loops (0.01 in my example).

These lazer rays can't kill anybody but if I were you I would combine them with my "vision cone" code in Aum28 (use a narrow cone for every eye). Set enable_scan = on for the player and when the event is triggered hurt the player. That's what I would do to create a good looking boss monster for my game!