AI - part 2 |
Top Previous Next |
Welcome to the second episode of the AI series! As you might remember, last time we have built a simple enemy that can move towards the player if it sees it, and attacks the player using its mace if the player is close enough.
Fortunately, last month’s enemy was unable to damage the player, but the things have changed for good ;). Open the ai4.c script file, run it, and then move the player until the enemy can see it; you will notice that the enemy can damage the player now by looking at player’s health value in the upper left corner of the screen.
In addition to this, the player will die if its health reaches zero.
We have discussed in detail the inner works of the enemy code in last month’s workshop, so we are only going to concentrate our energy on the new additions and changes to the code inside the ai4.c file.
PANEL* health_pan = { layer = 15; digits(4, 4, "Player's Health: %.f", arial_font, 1, players_health); flags = SHOW; }
The first addition is player’s health indicator, a simple panel that uses a “digits” instruction to display the needed text and the numerical value. Then, player’s while (1) loop has changed to something else:
action players_code() // attach this action to your player model { var movement_speed = 30; players_health = 100; VECTOR temp; set (my, INVISIBLE); player = my; my.emask |= ENABLE_SCAN; // make the player sensitive to c_scan instructions (no need to set an event function) while (players_health > 0) { my.pan -= 7 * mouse_force.x * time_step; temp.x = movement_speed * (key_w - key_s) * time_step; temp.y = movement_speed * (key_a - key_d) * 0.6 * time_step; temp.z = 0; c_move (my, temp.x, nullvector, IGNORE_PASSABLE | GLIDE); camera.x = my.x; camera.y = my.y; camera.z = my.z + 30; camera.pan = my.pan; camera.tilt += 5 * mouse_force.y * time_step; wait (1); } camera.roll = -20; // the player is dead here }
We have added a players_health variable that will keep the loop running until its value reaches zero; if this happens, we get out of the loop, thus stopping player’s actions, and then we set camera.roll to a dead-like -20 degrees value.
The last change that was made can be found inside function enemy_attacks( ):
function enemy_attacks() { ent_animate(my, "attack", attack_percentage, ANM_CYCLE); // then play the "attack" animation attack_percentage += 6 * time_step; // 6 sets the "attack" animation speed vec_for_vertex(mace_tip, enemy_mace, 41); // the tip of the mace model is its 41st vertex (get this value using Med) c_scan(mace_tip.x, enemy_mace.pan, vector(360, 180, 70), IGNORE_ME | SCAN_ENTS | SCAN_LIMIT); if (you == player) // detected the player? Then hurt it! { players_health -= 5 * time_step; // 5 gives the damage factor players_health = maxv(players_health, 0); // don't allow the health to go below zero } }
We are getting the coordinates of the tip of the mace (given by the 41st vertex for this particular mace model), and then we scan around this point on a range of 70 quants. If we detect the player, we decrease its health with a factor that is given by 5 * time_step, making sure that the health doesn’t go below zero (that would look bad on the health display).
OK, so now we’ve got an enemy that can damage and kill the player, but wouldn’t it be fair to give the player the means to defend himself? What sort of weapon should our player get in order to be able to resist the furious enemy attacks? I know! Let’s give the player a rifle! Open ai5.c and prepare for action!
Player’s weapon is so powerful that it can kill the enemy using a single shot; we don’t want to lose our precious time trying to hit the enemy 10 times, and I’m pretty sure that nobody (excepting Chuck Norris, of course) can't resist being shot 10 times in real life situations.
These things being said, let’s examine the new code that makes it all happen:
action players_code() // attach this action to your player model { var movement_speed = 30; players_health = 100; VECTOR temp; ent_create ("weapon1.mdl", nullvector, attach_weapon1); set (my, INVISIBLE); player = my; my.emask |= ENABLE_SCAN; // make the player sensitive to c_scan instructions (no need to set an event function) while (players_health > 0) { my.pan -= 7 * mouse_force.x * time_step; temp.x = movement_speed * (key_w - key_s) * time_step; temp.y = movement_speed * (key_a - key_d) * 0.6 * time_step; temp.z = 0; c_move (my, temp.x, nullvector, IGNORE_PASSABLE | GLIDE); camera.x = my.x; camera.y = my.y; camera.z = my.z + 30; camera.pan = my.pan; camera.tilt += 5 * mouse_force.y * time_step; wait (1); } camera.roll = -20; // the player is dead here }
First of all, the player has gotten an ent_create line of code inside its action; that line creates the weapon model and tells it to run function attach_weapon1( ) which has its code listed below:
function attach_weapon1() { proc_mode = PROC_LATE; weapon1 = my; // I'm the gun set(my, PASSABLE); while (1) { vec_set (my.x, vector (20, -10, 15)); // set the proper gun offset in relation to the player vec_rotate (my.x, you.pan); vec_add (my.x, you.x); my.pan = you.pan; my.tilt = camera.tilt; wait (1); } }
The function tells the weapon to mode in sync with the player and gives it a name – weapon1. The weapon is made passable and has offsets of 20, -10 and 15 quants on the x, y and z axis.
function gun_startup() { on_mouse_left = fire_bullets; }
function fire_bullets() { wait (1); snd_play(fire_wav, 100, 0); VECTOR bullet_pos[3]; // generate bullets from the 1st vertex of the weapon (get the vertex number from Med) vec_for_vertex(bullet_pos, weapon1, 1); ent_create("bullet.mdl", bullet_pos, move_bullets); }
The weapon will fire whenever the player presses the left mouse button; when this happens, we play a sound, and then we generate the bullet using the 1st vertex of the weapon, telling it to run the function named move_bullets( ):
function move_bullets() { VECTOR bullet_speed[3]; // stores the speed of the bullet // the bullet is sensitive to impacts with entities and level blocks my.emask |= (ENABLE_IMPACT | ENABLE_ENTITY | ENABLE_BLOCK); my.event = remove_bullets; // when it collides with something, its event function (remove_bullets) will run my.skill88 = 1357; my.pan = weapon1.pan; my.tilt = weapon1.tilt; bullet_speed.x = 150 * time_step; // adjust the speed of the bullet here bullet_speed.y = 0; // the bullet doesn't move sideways bullet_speed.z = 0; // or up / down on the z axis while (my) // this loop will run for as long as the bullet exists (it isn't "NULL") { // move the bullet ignoring its creator (weapon1) c_move (my, bullet_speed, nullvector, IGNORE_PASSABLE | IGNORE_YOU); wait (1); } }
The function above will make the bullet sensitive to impact with entities and level blocks, telling it to run its event function (remove_bullets) when one of these impacts happens. We set its skill88 to 1357 (a weird value that uniquely identifies the bullet), and then we start moving it using the orientation of the weapon and a speed that’s given by 150 * time_step. The bullet will move for as long as it exists using a c_move instruction that ignores the passable entities and its creator (YOU, the gun).
function remove_bullets() // this function runs when the bullet collides with something { wait (1); // wait a frame to be sure (don't trigger engine warnings) ent_remove (my); // and then remove the bullet }
Function remove_bullets is very simple; it waits for a frame in order to avoid any engine warnings, and then it removes the bullet.
It’s time to examine the code that makes the enemy die:
action enemy_1() { VECTOR temp, enemy_target; ANGLE temp_angle; // wait until the player model is loaded while (!player) {wait (1);} my.emask |= (ENABLE_IMPACT | ENABLE_ENTITY); my.event = enemy_was_hit; // run the enemy event function when it collides with an entity my.skill99 = 100; // stores the enemy's health ent_create("mace.mdl", my.x, attach_mace); while (my.skill99 > 0) // run this loop for as long as the enemy is alive { c_scan(my.x, my.pan, vector(120, 60, 1000), IGNORE_ME | SCAN_ENTS | SCAN_LIMIT); if (you == player) { if (vec_dist (player.x, my.x) > 110) // the enemy didn't come close enough to the player yet? { vec_set(enemy_target, player.x); vec_sub(enemy_target, my.x); vec_to_angle(temp_angle, enemy_target); my.pan += ang(temp_angle.pan - my.pan) * 0.25 * time_step; ent_animate(my, "run", run_percentage, ANM_CYCLE); // then play the "run" animation c_move (my, vector(10 * time_step, 0, 0), nullvector, IGNORE_PASSABLE | GLIDE); run_percentage += 6 * time_step; // 6 = "run" animation speed } else // the enemy is close enough to the player here { vec_set(temp, player.x); vec_sub(temp, my.x); vec_to_angle(my.pan, temp); my.tilt = 0; enemy_attacks(); // so let's play the enemy's "attack" animation } } else // the player wasn't detected { enemy_stands(); // so let's play the enemy's "stand" animation } wait (1); } // the enemy is dead here my.skill40 = 0; // skill40 controls the "death" animation while (my.skill40 < 95) // don't play all the animation frames because the result doesn't always look good { ent_animate(my, "death", my.skill40, NULL); // play the "death" animation my.skill40 += 2 * time_step; // "death" animation speed wait (1); } set (my, PASSABLE); // the corpse will be passable from now on }
The first change is the enemy’s while (1) loop, which has now become a while (my.skill99 > 0) loop. As you can guess, we are going to store the enemy’s health inside its skill99, something we didn’t want to do for the player because its health is a global value, which was supposed to be displayed on a panel.
As soon as the enemy dies, we set its skill40 to zero, and then we run another while loop which will play the “death” animation frames. Finally, the corpse is made passable, so it won’t stand in player’s way.
OK, so now we have got an enemy and a player that can attack and kill each other, so we can move on with the AI part of the workshop. Open the ai6.c script and then run it; you will see that this time the level will look a bit different.
I have added several obstacles in the level; the enemy will try to chase down the player just like before, but if it gets stuck (is unable to move) for 50 frames or more, it will rotate sideways and move for 1.5 seconds in that direction, and then it will try to reach the player once again. This is why you can also see the “Frames Blocked” indicator at the top of the screen.
This is how our basic obstacle avoidance algorithm works; let’s see how it was implemented. Fortunately, everything takes place inside the enemy_1( ) action.
action enemy_1() { VECTOR temp, enemy_target; VECTOR pos1, pos2; ANGLE temp_angle; var chasing_player = 0; // wait until the player model is loaded while (!player) {wait (1);} my.emask |= (ENABLE_IMPACT | ENABLE_ENTITY); my.event = enemy_was_hit; // run the enemy event function when it collides with an entity my.skill99 = 100; // stores the enemy's health ent_create("mace.mdl", my.x, attach_mace); while (my.skill99 > 0) // run this loop for as long as the enemy is alive {
The line of code below stores the enemy’s position inside the pos2 vector.
vec_set(pos2.x, my.x); c_scan(my.x, my.pan, vector(120, 60, 1000), IGNORE_ME | SCAN_ENTS | SCAN_LIMIT); if (you == player) { if (vec_dist (player.x, my.x) > 110) // the enemy didn't come close enough to the player yet? { vec_set(enemy_target, player.x); vec_sub(enemy_target, my.x); vec_to_angle(temp_angle, enemy_target); my.pan += ang(temp_angle.pan - my.pan) * 0.25 * time_step; ent_animate(my, "run", run_percentage, ANM_CYCLE); // then play the "run" animation c_move (my, vector(10 * time_step, 0, 0), nullvector, IGNORE_PASSABLE | GLIDE); run_percentage += 6 * time_step; // 6 = "run" animation speed chasing_player = 1; } else // the enemy is close enough to the player here { vec_set(temp, player.x); vec_sub(temp, my.x); vec_to_angle(my.pan, temp); my.tilt = 0; enemy_attacks(); // so let's play the enemy's "attack" animation chasing_player = 2; } } else // the player wasn't detected { chasing_player = 0; enemy_stands(); // so let's play the enemy's "stand" animation }
The line of code below stores the enemy’s current position inside the pos1 vector.
vec_set(pos1.x, my.x); wait (1);
If the distance between the pos1 and pos2 vectors is smaller than 0.1 and the enemy is chasing the player, this means that the enemy is stuck (unable to move). If this is true, the frames_blocked counter is increased by 1 each frame. When the enemy is unable to move for 50 consecutive frames, we store its current pan angle inside its own skill55, and then we rotate it 90 degrees (sideways) with a speed given by 50 * time_step.
// the enemy is supposed to chase the player, but it can't move? if ((abs(vec_dist(pos1.x, pos2.x)) < 0.1) && (chasing_player == 1)) { frames_blocked += 1; // frames_blocked will store the number of consecutive frames when the enemy is unable to move } else // the enemy managed to move? frames_blocked = 0; // then let's reset frames_blocked if (frames_blocked > 50) // the enemy was unable to move for 50 consecutive frames? { my.skill55 = my.pan; while (my.pan < (my.skill55 + 90)) { my.pan += 50 * time_step; wait (1); }
The lines of code below will move the enemy sideways for 1.5 seconds, while playing its “run” animation. Then, another loop rotates the enemy to its initial pan angle.
my.skill44 = 0; while (my.skill44 < 1.5) // move sideways for 1.5 seconds { my.skill44 += time_step / 16; ent_animate(my, "run", run_percentage, ANM_CYCLE); // play the "run" animation run_percentage += 6 * time_step; // 6 = "run" animation speed c_move (my, vector(10 * time_step, 0, 0), nullvector, IGNORE_PASSABLE | GLIDE); wait (1); } my.skill55 = my.pan; while (my.pan > (my.skill55 - 90)) { my.pan -= 50 * time_step; wait (1); } } } // the enemy is dead here my.skill40 = 0; // skill40 controls the "death" animation while (my.skill40 < 95) // don't play all the animation frames because the result doesn't always look good { ent_animate(my, "death", my.skill40, NULL); // play the "death" animation my.skill40 += 2 * time_step; // "death" animation speed wait (1); } set (my, PASSABLE); // the corpse will be passable from now on }
This concludes this month’s AI series; next time we will get into more complex AI snippets. |