AI - part 6

Top  Previous  Next

Welcome to another episode from the AI workshop series! This month we have got a real enemy that fires real bullets at the player and can kill it. But don't worry, the player isn't helpless; it's got a sniper gun that can kill the enemy after 3 successful shots. To level the playing field I have give the enemy the ability of killing the player with a single shot, though. Why is that? To begin with, while our path finding algorithm works great, it can't do more than finding the shortest path between two nodes. This means that our enemy will move from point A to point B, and then, after reaching point B, will try to shoot at the player IF the player is still near point B. If the player has moved away from point B, the enemy must get the new path to point C, where the player is now located, and so on.

 

This means that all you need to do is move around at all times; this will make the enemy spend more time moving from one place to the other, rather than trying to kill you. On the other hand, you have the ability of shooting the enemy while it is traveling from one node to the other, something that it doesn't have (yet).

 

OK, so let's see how our demo starts! Everything begins with a 3, 2, 1, GO! sequence.

 

aum111_ai1

 

Then, start moving around using the WSAD keys and the mouse, and fire bullets using the left mouse button. If you manage to shoot the enemy 3 times, it will go down like a sack of rotten bones ;)

 

aum111_ai2

 

On the other hand, it is true that if it manages to shoot you down (you can dodge its bullets if you are fast, trust me!) you'll be seeing the things from a different perspective.

 

aum111_ai3

 

The good news is that no matter what happens, you can restart the demo as many times as you want by simply pressing the "R" key.

 

What about the code that makes it all happen? Besides adding to the AI code, I have included two fresh script files in the main script: player.c and weapons.c. The first one is a slightly modified version of the template shooter player code, so I won't go through it once again - it's heavily commented anyway. The weapons.c script is very simple - see for yourself!

 

PANEL* sniperscope_pan =

{

       layer = 15;

       bmap = sniperscope_tga;

       flags |= (OVERLAY | SHOW);

}

 

PANEL* ready_pan =

{

       layer = 15;

       bmap = three_png;

}

 

Everything begins with two simple panel definitions; the first one will be used for the sniper gun overlay, while the second one is used to display the 3, 2, 1, GO! sequence.

 

function ready_go()

{

       ready_pan.bmap = three_png;

       set (ready_pan, SHOW);

       wait (-4);

       ready_pan.bmap = two_png;

       wait (-1);

       ready_pan.bmap = one_png;

       wait (-1);

       ready_pan.bmap = go_png;

       snd_play(go_wav, 100, 0);

       movement_enabled = 1;

       wait (-1);

       reset (ready_pan, SHOW);

}        

 

The function above is called at game start from function main; its role is to switch the 3, 2, 1, GO! bitmaps, playing a sound when the player is ready to start moving. The movement_enabled variable keeps the player immobile for as long as its value is zero. The last two lines of code tell the engine to wait for a second, and then hide the "GO!" panel.

 

function fire_bullets() // this function is triggered by any left mouse button push

{

  vec_set(trace_coords.x, vector(10000, 0, 0)); // the sniper gun has a firing range of 10,000 quants

  vec_rotate(trace_coords.x, camera.pan);

  vec_add(trace_coords.x, camera.x);

  c_trace(camera.x, trace_coords.x, IGNORE_ME | IGNORE_PASSABLE | SCAN_TEXTURE | ACTIVATE_SHOOT); // trace 10,000 quants in front of the player

  if (you) // hit one of the entities in the level?

  {

         // checking if the entity that was hit is the enemy

         if (you.skill47 == 5678)

         {                  

                 you.skill88 -= 35; // decrease enemy's health

                 ent_playsound(you, enemyhit_wav, 1500);

         }

  }

  snd_play(sniperfired_wav, 100, 0); // bullet fired sound effect

}

 

Function fire_bullets() is triggered every time the player presses the left mouse button. We trace 10,000 quants in front of the player (that would be the weapon range) and we check if we have hit an entity (the "you" pointer would be valid in this case) or not. If we have hit an entity and its skill47 value is set to 5678 (a weird value we have picked to uniquely identify the enemies), we decrease the enemy's health by 35, thus making sure that it will die from 3 shots (its health value is set to 100). Not only that, but we are also telling it to play a "hit" sound. Finally, we play a sniperfired_wav bullet sound effect.

 

function sniper_startup()

{

       on_mouse_left = fire_bullets;

       while (1)

       {

               sniperscope_pan.scale_x = screen_size.x / bmap_width(sniperscope_tga);

               sniperscope_pan.scale_y = screen_size.y / bmap_height(sniperscope_tga);

               ready_pan.pos_x = screen_size.x / 2 - bmap_width(one_png) / 2;

               ready_pan.pos_y = screen_size.y / 2 - bmap_height(one_png) / 2;                

               wait (1);

       }

}

 

The last function inside weapons.c simply aligns the sniper scope and the "GO!" panels in the center of the screen.

 

OK, so now that we've got the weapon code out of the way, what's changed in the ai10.c script?

 

function main()

{

       on_r = restart_game;

       

       // reset the variables - needed after the game is restarted

       movement_enabled = 0; // don't allow the player to move until the GO! message is displayed        

       number_of_nodes = -1; // stores the number of nodes (up to 300 nodes = 0...299 in this demo)

       current_node = 0; // the current node that scans for the other nodes

       start_node = 0; // the node that was clicked with the left mouse button, starting point for enemies in future demos

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

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

       index = 0; // used as a counter from 0 to the maximum number of nodes that were placed in the level

       distances_computed = 0; // will be set to 1 when the array is filled with the distances between nodes

       ...

       ready_go(); // displays the 3.. 2.. 1.. GO! message

       camera.arc = 90;

       if (soundtrack_playing == 0) // don't start the soundtrack more than once after game restart

       {

               soundtrack_playing = 1;

               media_loop("soundtrack.mp3", NULL, 50);

       }

}

 

function restart_game()

{

       players_health = 100;

       main();        

}

 

First of all, we have added a tiny restart_game( ) function, which simply sets player's health to 100 again and calls function main( ). Then, we have reinitialized a few variables inside function main, thus making sure that the path finding algorithm works as expected. The ready_go( ) function is called, displaying the 3, 2, 1, GO! message, and then we set the camera.arc value to 90. It's important to choose big camera.arc values when designing shooter games; this way, you are giving the players the illusion of moving at a faster speeds in larger levels without affecting the CPU performance. We have added a soundtrack as well; since it shouldn't restart each time when we restart the demo, it will only play once, in a loop.

 

The enemy code has changed as well; I have highlighted the new lines of code.

 

action my_enemy()

{

       var anim_percentage;

       var shoot_percentage;

       VECTOR temp, marker_pos, bullet_coords;

       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 enemy, used in the trace_back function

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

       while (player)

       {

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

               {

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

                       vec_set(temp, player.x);

                       vec_sub(temp, my.x);

                       vec_to_angle(my.pan, temp);

                       my.tilt = 0;

 

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

                       {                        

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

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

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

                                       }                                        

                               }

                       }

                       else

                       {

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

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

                       }

                       wait (1);

               }

 

Since our enemy can die now, its health value is stored inside its own skill88. This means that we need to use a "break" instruction whenever its health goes below zero; this way, its actions will be stopped for good. If you remember last month's discussion, you know that the highlighted lines of code are run when the enemy has reached its destination and is now seeing the player. As soon as this happens, we rotate the enemy towards the player, while keeping its tilt angle set to zero. Why are we doing this? The player might be upstairs, and it wouldn't look too good to have the enemy change its tilt angle, while trying to orient itself towards the player.

 

If player's health is above zero, the enemy will shoot its bullets, with a speed given by 6 * time_speed. The actual shooting frame is chosen at the beginning (less than 2%) of the shooting animation. The bullet is fired from the 7th vertex of the enemy model; a sound is played at the same time. It wouldn't make sense for the enemy to keep firing at player's (dead) corpse, so the "shoot" animation will switch to "stand" if player's health goes below zero.

 

               else

               {

                       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

                       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

                               

                               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

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

                                       wait (1);

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

                                       {

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

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

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

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

                                               wait (1);

                                       }

                                       d1_marker = NULL;

                                       i -= 1;

                                       wait (1);

                               }

                       }        

               }

       }

       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 enemy is dead here, so we get to hear its scream, and then we play its "death" animation. The model has a small "death" animation problem, so we're only playing 90% of its "death" animation. Finally, we make its corpse passable, allowing the player to pass through it.

 

So this was our first "real" demo, with an enemy chasing the player, firing bullets at it, and so on. It's got a few shortcomings as it is now, I agree, but they have nothing to do with the inner works of the AI algorithm. The good news is that we'll take care of them all in future AI  episodes, so stay tuned!