Perfect AI - part IV

The 4th Perfect AI episode adds a few more important features to the code we've got so far. Don't forget to read the articles in Aum27...29 to see how it all started.

Open level4, build it and run it using ai4.wdl:

Take a deep breath and then start running; the four little guys will try to hunt you down. Use "W", "S", "A", "D" to move, left mouse button to shoot. Try to survive as much time as possible; take a look at the four paths displayed on the screen and see how they change depending on your position. I could have used many more enemies in this demo, but trust me, four are enough in this level. Oh, you can kill all the enemies; I managed to do it.

Some of the code from Aum29 has changed, more code was added; we will discuss all the new or modified actions and functions, placed below this header in ai4.wdl:

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// new stuff for the 4th Perfect AI episode
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

define health skill42;
define start_node skill43;
define closest_node skill44;

We are using a simple panel to display the paths for the 4 enemies, player's health and the target node (the destination for the enemies):

panel ai_pan // displays the numerical values on the screen
{
pos_x = 0;
pos_y = 0;
layer = 10;
digits = 80, 72, 2, system_font, 1, player.health;
digits = 80, 85, 2, system_font, 1, target_node;

digits = 120, 0, 4, system_font, 1, path[0]; // display the first 15 points on the shortest path
digits = 150, 0, 4, system_font, 1, path[1]; // should be enough for everybody
digits = 180, 0, 4, system_font, 1, path[2];
// many more digits here
............................................................

flags = overlay, refresh, visible;
}

This demo uses 30 nodes that exchange information; let's take a look at the action that drives them:

action node // using 30 nodes in this demo
{
my.invisible = on;
my.enable_scan = on;
my.event = trace_back;
my.skill47 = 1234;
my.passable = on;
node_id += 1;
my.skill48 = node_id;
nodex[my.skill48] = my.x;
nodey[my.skill48] = my.y;
while (node_id < (max_nodes - 1)) {wait (1);}

Every node is invisible and sensitive to scan; when a node is scanned, it runs its trace_back event. We set skill47 = 1234 for every node and then we make it passable, because the player and its enemies should be able to pass through it. Every node gets a unique id number, which is stored in its own skill48. We also store the x and y coordinates of the node in two arrays named nodex and nodey and then we wait until all the nodes are placed in the level.

while (current_node <= node_id)
{
if (current_node == my.skill48)
{
temp.x = 360;
temp.y = 30;
temp.z = 1000;
scan_entity (my.x, temp);
wait (1);
current_node += 1;
}
wait (1);
}
distances_computed = 1;

The nodes start scanning each order in this order: node0 scans node1... node29, then node1 scans node0... node29, etc. We are using a horizontal scanning range of 360 degrees, a vertical scanning angle of 30 degrees and a scanning range of 1000 quants. As soon as the last node (29) has scanned node0... node28, a var named distances_computed is set to 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);
}
}

Every node stores the distance from it to the player in an array named node_to_player; if the player comes closer than 500 quants to the node, the node traces from its position to the player. If the node can "see" the player, it sets see_player to 1, otherwise see_player is set to 0. All these things happen 10 times a second.

Let's see what happens when a certain node is scanned by another node:

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);
index = you.skill48 + my.skill48 * max_nodes;
node_to_node[index] = vec_dist(my.x, you.x);
}
}

If one of the nodes is scanned, we check if the scanner has skill47 = 1234; if this is true, we know that the scanner was a node. We store the "you" entity (trace would destroy it) in skill45 and we trace back from the scanned node to the scanner. If the node can "see" the node that has scanned it, it restores the "you" pointer and stores the distance between the two nodes in the vector named node_to_node (a bidimensional array that was simulated using a simple array).

else
{
my.skill45 = handle(you); // store "you" because trace will destroy it
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.x, you.x) == 0) // if this node can "see" the enemy that scanned it
{
you = ptr_for_handle(my.skill45); // restore the "you" pointer
dist_to_enemy[my.skill48 + max_enemies * you.skill48] = vec_dist (my.x, you.x);
if ((dist_to_enemy[my.skill48 + max_enemies * you.skill48] < you.closest_node) && (dist_to_enemy[my.skill48 + max_enemies * you.skill48] != 0))
{
you.closest_node = dist_to_enemy[my.skill48 + max_enemies * you.skill48];
}
wait (2);
if (dist_to_enemy[my.skill48 + max_enemies * you.skill48] == you.closest_node)
{
you.start_node = my.skill48;
}
}
else
{
you = ptr_for_handle(my.skill45);
dist_to_enemy[my.skill48 + max_enemies * you.skill48] = 0;
}
}
}
}

The "else" branch runs when a certain node is scanned by one of the enemies; we store "you" and we trace from the node to the enemy that has scanned the node, storing the distance from the node to the enemy in the array named dist_to_enemy. If the distance between the node and the enemy is smaller than the older distance stored by the enemy (closest_node = skill44) and it is bigger than zero, this node will be set as the closest node to the enemy. We give the rest of the nodes the chance to change this for two frames (in case that one of them is closer to the enemy) and then we set the starting node for the enemy as the node that is the closest to the enemy. If the node can't see the enemy, its dist_to_enemy will be set to zero.

All these things happen because the enemy needs to know which node is the closest to it and sees it. The enemy will move towards the closest node and then it will run the function that generates the shortest path (more on that a little later).

Let's discuss my favorite action:

action player1
{
player = me;
my.health = 100;
my.push = -1;
my.enable_entity = on;
my.enable_impact = on;
my.event = decrease_health;
while (my.health > 0)
{
my.pan += 8 * (key_a - key_d) * time;
my.skill1 = 15 * (key_w - key_s) * time;

The player has 100 health points; its push value is -1, so it won't pass through walls (their push is zero). The player is sensitive to other entities and runs its decrease_health event when something hits it. As long as the player is alive, it can rotate using "A" and "D" and moves forward / backward with "W" and "S".

if (key_w + key_s > 0)
{
ent_cycle("walk", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
}
else
{
ent_cycle("idle", my.skill46);
my.skill46 += 2 * time;
my.skill46 %= 100;
}
if (mouse_left == 1)
{
players_bullet();
}
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
wait (1);
}

If the player is moving ("W" or "S" is pressed), the model plays its "walk" animation in a loop, otherwise it plays its "idle" animation in a loop. If the player presses the left mouse button, the function named players_bullet will run. The player model moves ignoring passable entities and glides along the walls.

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

The lines above run when the player has died: they run player's "death" animation once. Let's see the most complicated function - the one attached to the enemies:

action enemy
{
my.push = -2;
my.enable_entity = on;
my.enable_impact = on;
my.health = 100;
my.event = enemy_event;
enemy_id += 1;
my.skill48 = enemy_id;
my.skill47 = 5678;
while ((player.health > 0) && (my.health > 0))
{
trace_mode = ignore_me + ignore_models + ignore_passents;
if (trace (my.pos, player.pos) == 0)
{
my.skill46 = 0;
shoot_bullet();
while (my.skill46 < 100)
{
ent_cycle("standshoot", my.skill46);
my.skill46 += 15 * time;
wait (1);
}
}

The enemy has its "push" set to -2; this way the enemies can't kill each other because their bullets have push = -1. The enemy is sensitive to other entities and to impact; its health is set to 100 and its event is enemy_event. Every enemy gets a unique id number (0...3 in this demo) which is stored in its own skill48. We set skill47 to 5678 - that's how we can identify if a certain entity is an enemy or not.

The while loop continues to run if the player and the enemy are alive. The loop traces from the enemy to the player; if the enemy can see the player, is starts shooting at it using the function named shoot_bullet, and playing its "standshoot" animation in a loop.

else
{
my.closest_node = 999999;
temp.x = 360;
temp.y = 360;
temp.z = 800;
scan_entity (my.x, temp);
if (vec_dist (my.x, player.x) > 1000)
{
my.skill46 = 0;
while (my.skill46 < 100)
{
ent_cycle("alert", my.skill46);
my.skill46 += 2 * time;
wait (1);
}
}
else
{
wait (2);
}
while (function_busy == 1) {wait (1);}
function_busy = 1;

If the enemy can't see the player, its sets its closest_node distance to a huge value and then it scans the nodes nearby because it needs to detect the closest node to it. If the player is farther than 1000 quants, the enemy plays its "alert" animation, pretending to think before choosing a new path that leads to the player. If the distance between the enemy and the player is smaller than 1000 quants, the enemy will wait for 2 frames before choosing a new path.

I have used a single function that computes the paths for all the enemies. When the function is busy, the var named function_busy will be set to 1; otherwise it will be set to 0. The enemy waits until function_busy is 0 and then it takes control over it, setting function_busy to 1.

path_index = 0;
while (path_index < max_nodes)
{
path[path_index + max_enemies * my.skill48] = 0;
path_index += 1;
}
path_index = 0;
path[path_index + max_enemies * my.skill48] = target_node;
snd_play (getpath_wav, 100, 0);
if (my.start_node != target_node)
{
node_index = find_path(my.start_node, target_node, my.skill48, path_index);
wait (2);
function_busy = 0;

At first, we reset the array named "path", because we need to get rid of the previously stored path from the enemy to the player. Target_node, the closest node to the player that can see the player, is the first path element. The engine will play a short sound, letting us know that one of the enemies needs to get the shortest path to the player. If the starting node and the target node are different, we run the function named find_path that takes four parameters and returns the result in node_index. We wait for the path to be computed and then free the function by setting function_busy to zero.

while (node_index >= 0)
{
temp.x = nodex[path[node_index + max_enemies * my.skill48]];
temp.y = nodey[path[node_index + max_enemies * my.skill48]];
temp.z = my.z;
vec_set (destination_node, temp);
vec_set (temp.x, destination_node.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
while ((vec_dist (destination_node.x, my.x) > 10) && (relax == 0))
{
ent_cycle("run", my.skill46);
my.skill46 += 10 * time;
my.skill46 %= 100;
my.skill1 = 10 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_passable + glide;
ent_move(my.skill1, nullvector);
if (trace (my.pos, player.pos) == 0)
{
node_index = -1;
}
wait (1);
}
node_index -= 1;
wait (1);
}
}
}
}

The shortest path to the player was returned by function find_path, now we need to move the enemy on that path! We retrieve the x and y coordinates for the nodes on the path, using enemy's height for z, and then we rotate the enemy towards the destination node. As long as the enemy hasn't come closer than 10 quants to the player and the player isn't dead yet (relax = 0), the enemy plays its "run" animation in a while loop and moves ignoring passable entities and gliding along the walls.

If the enemy can see the player (trace returns zero), node_index is set to -1, allowing the enemy to get out of the while loop, in order to compute a new path to the player, because the shortest path to the player has changed. Finally, node_index is decreased, allowing the enemy to move towards a new node as soon as it has come closer than 10 quants to the previous node.

// the player is dead, so switch to "stand" if you are alive
while (my.health > 0)
{
ent_cycle("stand", my.skill46); // play "stand" frames animation
my.skill46 += 4 * time; // "stand" animation speed
my.skill46 %= 100;
wait (1);
}
}

The lines of code above will run if the player had died; the enemy will switch to "stand" if it is alive and will play this animation in a loop. The function below computes and writes the actual path for every enemy that uses it:

function find_path(sn, tn, id, px)
{
var p_i = 0;
while (p_i < max_nodes)
{
if ((p_i != tn) && (visited[p_i + max_nodes * tn] == 0))
{
if (node_to_node[sn + max_nodes * tn] == node_to_node[sn + max_nodes * p_i] + node_to_node[p_i + max_nodes * tn])
{
px += 1;
path[px + max_enemies * id] = p_i;
next_node = p_i;
p_i = max_nodes - 1;
if (path[px + max_enemies * id] == sn)
{
return(px);
}
}
}
p_i += 1;
}
tn = next_node;
find_path(sn, tn, id, px);
}

The function takes four parameters:
- Start node (sn), the node that is the closest node to the enemy and can see the enemy;
- Target node (tn), the node that is the closest node to the player and can see the player;
- Enemy id (id), a number that tells the function which enemy needs to get the path to the player;
- Path index (px), used as an index for the shortest path to the player.

The function will search from the current node to the rest of the nodes; this demo uses 30 nodes, so every node has to check 29 more nodes. If the current node can see one or more of these nodes and if we have a node that is a part of the shortest path (the shortest path includes it), we move the the next path array element and we store the node number inside the path[] array (a bidimensional array that was created using a simple array). We store the new node on the shortest path in next_node, because it will loose its value right away, and then we stop testing other values (p_i = max_nodes - 1) because we want to eliminate other paths that might have the same length.

If we have reached the node that is the starting point for the enemy, we get out of the function, returning the value stored in px. If the function continues to run, p_i is incremented; if the current node can't see another node, tn = next_node sets the next node on the path as target and the (recursive) function is run again, searching for the shortest path but being one step closer to the final result.

The hard part is over; the rest of the functions are simple:

function shoot_bullet()
{
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
if (player.health > 0)
{
ent_create (bullet_mdl, my.pos, move_bullet);
}
}

That's the function that runs if the enemy can see the player; it rotates the enemy towards the player and if player's health is above zero, it starts creating bullets that use the function move_bullet():

function move_bullet()
{
my.skill47 = 5;
my.pan = you.pan;
my.push = -1;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_bullet;
while (my != null)
{
my.skill1 = 15 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable + ignore_push;
ent_move(my.skill1, nullvector);
wait (1);
}
}

I have set skill47 to 5 for every bullet fired by the enemies; the bullet and the enemy that creates it have the same pan angle. I have set push = -1 for the bullet, because this way the bullet can pass through enemies without hurting them (their push was set to -2) but hits the player (its push was set to -1). The bullet is sensitive to other entities and to level blocks; its event function is named remove_bullet. As long as the bullet continues to exist, it moves with the speed given by skill1, ignoring its creator, passable entities and other entities that have their push value below -1.

function remove_bullet()
{
if ((my.skill47 == 5) && (you.skill47 == 5))
{
my.passable = on;
wait (2);
ent_remove (me);
return;
}
my.event = null;
ent_playsound (my, hit_wav, 1000);
sleep (0.1);
ent_remove (me);
}

The function above runs when the bullet hits something. If two enemy bullets have collided, one of them is made passable and after two frames it is removed; if the bullet hits something else, it stops responding to other event, plays a hit_was sound and disappears after 0.1 seconds.

function decrease_health()
{
if (you.skill47 == 5678)
{
my.health = 0;
}
else // hit by a bullet?
{
my.health -= 1;
}
if (my.health <= 0)
{
my.event = null;
snd_play (death_wav, 40, 0);
}
}

Function decrease_health is the one that runs when the player is hit by something; if the player has collided with one of the enemies (you.skill47 = 5678), the player will die instantly because the enemy will stab it (more on that a little later). If the player is hit by a bullet, it looses 1 health point every tick; the bullet will be active for 0.1 seconds so I'll let you do the math. If player's health goes below zero, the player will stop responding to events and it will play the death_wav sound. Let's see the event function associated to the enemies:

function enemy_event()
{
if (you.skill47 == 5678)
{
if ((vec_dist (my.x, player.x)) > (vec_dist (you.x, player.x)))
{
my.passable = on;
sleep (0.5);
my.passable = off;
}
}

If the enemy has collided with another enemy, and if "you" are closer than "my" to the player, "my" will become passable, allowing "you" to pass first; this situation lasts for 0.5 seconds.

if (you == player)
{
my.event = null;
player.health = 0;
vec_set (temp.x, player.x);
vec_sub (temp.x, my.x);
vec_to_angle (my.pan, temp);
my.skill46 = 0;
while (my.skill46 < 90)
{
ent_cycle("point", my.skill46);
my.skill46 += 1 * time;
wait (1);
}
relax = 1;
}

If the enemy collides with the player, it will stop reacting to other events. The player will loose its health and the enemy will turn towards the player; the enemy will play its "point" (stab) animation once and then the var named "relax" will be set to 1, telling all the enemies to relax (to stop and switch their animations to "stand").

if (you.skill47 == 1)
{
my.health -= 1;
if (my.health <= 0)
{
my.event = null;
my.invisible = on;
my.passable = on;
}
}
}

If the enemy is hit by player's bullet (you.skil47 = 1), it will loose 1 health point every tick. If the enemy is dead, it will stop reacting to other events, and then it will become invisible and passable.

function players_bullet()
{
proc_kill(4);
while (mouse_left == 1) {wait (1);}
snd_play (bullet_wav, 50, 0);
ent_create (bullet_mdl, player.pos, move_players_bullet);
}

The function above runs when the player presses the left mouse button; proc_kill(4) will stop all the other instances of the current function. We wait until the left mouse button is released (we disable auto fire), we play a sound and then we create a bullet that is driven by function move_players_bullet():

function move_players_bullet()
{
my.skill47 = 1;
my.pan = you.pan;
my.tilt = you.tilt;
my.passable = on;
my.enable_entity = on;
my.enable_impact = on;
my.enable_block = on;
my.event = remove_bullet;
my.ambient = 100;

Every bullet fired by the player has its skill47 set to 1; the bullet and the player have the same pan and tilt angle. The bullet is passable at first; it is sensitive to other entities and to level blocks. The bullets fired by the player and by the enemies share the same event function - remove_bullet().

while (my != null)
{
if (vec_dist (player.x, my.x) > 30) {my.passable = off;}
my.skill1 = 50 * time;
my.skill2 = 0;
my.skill3 = 0;
move_mode = ignore_you + ignore_passable;
ent_move (my.skill1, nullvector);
wait (1);
}
}

The bullet will stop from being passable when its distance to the player has become bigger than 30 quants. The bullet moves with the speed given by skill1, ignoring its creator and all the passable entities.

"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!" That's what I wrote at the end of the article that has appeared last month, so some of you might feel a little frustrated. The truth is that I got attached to the maze level :) and I wanted you to be able to see the enemies chasing the player all over the map.

Well, I like to keep my promises, so I have added a new, simple player action named "player2" at the end of the ai4.wdl file. Open the level and replace the action attached to the player (player1) with the new action (player2) to change the camera to a 1st person shooter experience. Use "W", "S", "A", "D" to move and the mouse to rotate and shoot.

What comes next month? New code and a playable level with a big number of nodes and enemies, monsters that wait until you get closer to them and then attack you, and more!

Stationary weapons

This article will show you how to create stationary weapons: cannons, big machine guns, etc.

The most complicated piece of code is listed below:

action sweapon
{
while (player == null) {wait (1);}
player._health = 100;
my.passable = on;
while (player._health > 0)
{
if (player_immobile == 0)
{
while (vec_dist (player.x, my.x) > 100) {wait (1);}
vec_set (players_speeds, strength);
vec_set (players_angles, astrength);
vec_set (strength, nullvector);
vec_set (astrength, nullvector);
player_immobile = 1; // can't move
while (key_any == 1) {wait (1);}
player.pan = 0;
player.tilt = 0;
}

We wait until the player is created and then we set its health to 100 (in case you forgot to do that in the action associated to the player). The stationary weapon (a cannon, in my example) will be passable.

Action sweapon will continue to run if player's health is greater than zero; I have defined a variable named "player_immobile" that is set to 0 when the player can move and is set to 1 when the player is immobile. If the player can move, we wait until it has come closer than 100 quants to the cannon and then we copy its speed on x, y, z and its angular speed in players_speed and players_angles. The next lines of code set strength and astrength to zero, so the player will not be able to move or rotate from now on. Player_immobile is set to 1, we wait until all the keys are released and then we reset player's pan and tilt angles.

else // player_immobile == 1
{
player.x = -1250;
player.y = -728;
if ((key_cuu == 1) && (player.tilt < 30))
{
player.tilt += 2 * time;
}
if ((key_cud == 1) && (player.tilt > -10))
{
player.tilt -= 2 * time;
}
if ((key_cul == 1) && (player.pan < 30))
{
player.pan += 2 * time;
}
if ((key_cur == 1) && (player.pan > -30))
{
player.pan -= 2 * time;
}
my.pan = player.pan;
my.tilt = player.tilt;

If the player can't move, we set its x and y coordinates where we need them; I have got these values at runtime, using the debug panel. If we press the "up" arrow key and player's tilt angle is below 30 degrees, we increase the tilt angle. If we press the "down" arrow key and player's tilt is above -10 degrees, we decrease the tilt angle. The same thing happens with the pan angle; it changes its values from -30 to 30 degrees using the "left" and "right" arrow keys.

The last two lines of code are copying player's pan and tilt angle to the cannon.

if (key_ctrl == 1)
{
while (key_ctrl == 1) {wait (1);}
vec_for_vertex (rocket_coords, my, 190);
snd_play (fire_wav, 100, 0);
ent_create (rocket_mdl, rocket_coords, move_rocket);
}
if (key_space == 1)
{
vec_set (strength, players_speeds);
vec_set (astrength, players_angles);
player.pan = 0;
player.tilt = 0; // restore pan and tilt
player_immobile = 0; // can move
while (vec_dist (player.x, my.x) < 200) {wait (1);}
}
}
wait (1);
}
}

If we have pressed the "Ctrl" key on the keyboard, we wait until it is released (we disable autofire), we get the vertex coordinates for the rocket (vertex #190 for the cannon model), we play a sound and then we create a rocket that moves using the function named move_rocket.

If we press "space", we restore the initial strength and astrength values, allowing the player to move again. We set pan = 0, tilt = 0 and player_immobile = 0, telling the rest of the action that the player can move from now on. We allow the player to get away from the cannon if he doens't need the cannon anymore.

Let's get back to the function that moves the rocket:

function move_rocket()
{
my.enable_entity = on;
my.enable_block = on;
my.event = remove_rocket;
my.pan = camera.pan;
my.tilt = camera.tilt;
rocket_speed.y = 0;
rocket_speed.z = 0;
while (my != null)
{
rocket_speed.x = 100;
rocket_speed *= time;
move_mode = ignore_you + ignore_passable;
ent_move (rocket_speed, nullvector);
wait (1);
}
}

The rocket is sensitive to entities and level blocks; when it collides with one of the entities or with a level block, its event function named remove_rocket will run. The rocket has the same pan and tilt angles with the camera and its speed is given by rocket_speed.x. The rocket moves in a while loop, ignoring its creator (the cannon) and all the passable entities.

Curious to see the event function for the rocket? Ok, there you go:

function remove_rocket()
{
wait (1);
if (you != null)
{
you._health -= 150;
}
ent_create(explosion_pcx, my.pos, sprite_explosion);
my.event = null;
my.invisible = on;
wait (2);
ent_remove (me);
}

If the rocket hits an entity (you != null) it takes 150 health points from that entity, creates an explosion sprite, sets its event to null (so it won't react to anything else from now on), becomes invisible and it is removed after two frames. The function that drives the explosion sprite is listed below:

function sprite_explosion()
{
my.scale_x = 5;
my.scale_y = my.scale_x;
my.scale_z = my.scale_x;
my.passable = on;
my.flare = on;
my.bright = on;
my.ambient = 100;
ent_playsound (my, explosion_wav, 400);
while (my.frame < 7)
{
my.frame += 1 * time;
wait (1);
}
ent_remove (me);
}

The scale of the sprite is increased five times on x, y and z; this might come in handy if you want to replace the sprite with a model. The explosion is passable and has its "flare" and "bright" flags set; the ambient of the sprite is set to 100. We will hear a sound at the impact point and the explosion frames will be played in a loop; as soon as the last explosion frame was displayed, the sprite will be removed.