Artificial intelligence - part 2

Top  Previous  Next

This month we will get to see Ackbot in action; the code we'll get at the end of the workshop will make it behave properly, according to the state table we've discussed about in Aum55. But before we dive into the AI code, let's examine a few small changes and additions to the script.

 

aum56_workshop1

 

First of all, you will notice several "define" instructions, which, as you should recall, simply give some meaningful names to some numbers and two skills. Then, you will notice that the player has got 100 health points, which are stored in its own skill10, aka "health". I didn't want us to be invincible, so I have decided to make the player sensitive to impact with other entities; as soon as the player is hit by an entity, its event function (hurt_player) will run.

 

The first "while" loop inside "players_code" will run for as long as the player is alive (its health is greater than zero); it takes care of player's movement, firing and camera code. The loop will stop running when the player runs out of health points, giving control to the second loop, which makes the camera change its tilt and roll angles until the player appears to be laying down on its back, looking at the ceiling (camera.tilt reaches 90 degrees).

 

Let's take a good look at the enemy code now:

 

aum56_workshop2

 

See? It doesn't look that complicated! We define three local variables, which will take care of the animation speed when the enemy is standing still, running or dieing, and then we set the "polygon" flag for the enemy. This flag forces our enemy model to use its real shape when it comes to collisions, and not the default ellipsoid that is used for faster collisions.

 

aum56_workshop3          aum56_workshop4

 

Take a look at the two pictures from above; if you shoot the model inside the invisible ellipsoid that surrounds it when the "polygon" flag isn't set, each (red) shot will be registered as a valid shot, hurting the enemy. On the other hand, if you set the "polygon" flag on, the (red) shots will be ignored; the model will react only to the bullets that are actually hitting its mesh.

 

Why would we want to go through all the trouble with the invisible bounding ellipsoid then? There are two main reasons:

1) the collision detection process is much faster this way, so our game will run faster;

2) the models won't get stuck. I wouldn't want to see two enemy models glued together, unable to separate themselves because both of them use polygon-based collision detection and their arms have overlapped.

Many games, including many successful AAA games continue to use bounding boxes or ellipsoids for the enemies; you should try to do the same thing whenever it is possible.

 

Ok, back to our code then:

 

aum56_workshop2

 

The enemy has 100 skill points, being sensitive to impact with other entities (player's bullets). If the enemy is hit by an entity, its "got_shot" event function will run. Ackbot starts the game in its "idle" status; the first loop will continue to run for as long as Ackbot's status isn't changing to "dead".

 

if (my.status == idle)

{

       ent_animate(my, "stand", idle_percentage, anm_cycle);

       idle_percentage += 3 * time;

       if (vec_dist (player.x, my.x) < 1000)

       {

               my.status = attacking;

       }

}

 

We have set the "idle" status for our enemy at the beginning of the action; if this is still true, the enemy will play its "stand" animation with the speed given by "3 * time". If the player comes closer than 1000 quants to the bot, we are setting the "attacking" status, alerting the enemy.

 

if (my.status == attacking)

{

       vec_set(temp, player.x);

       vec_sub(temp,my.x);

       vec_to_angle(my.pan, temp);

 

The bot will start attacking the player when the distance between it and the player becomes smaller than 1000 quants or when the player shoots it, even if the distance from Ackbot to the player is bigger than 1000 quants. If one of these two is true, those 3 lines of code will make the bot turn towards the player; read the workshop from Aum52 if you have forgotten how vec_to_angle works.

 

       if (vec_dist (player.x, my.x) > 500)

       {

               c_move (my, vector(10 * time, 0, 0), nullvector, glide);

               ent_animate(my, "run", run_percentage, anm_cycle);

               run_percentage += 6 * time;

       }

 

Please don't forget that our bot is still in its "attacking" status. If the player is farther than 500 quants, Ackbot will move towards the player with the speed given by "10 * time", playing its "run" animation with the speed given by "6 * time". The bot will glide along the walls; this will make it look a bit more "human" while trying to avoid small obstacles.

 

       else

       {

               ent_animate(my, "alert", 100, null);

       }

 

If the bot is closer than 500 quants to the player, there's no need to come even closer so the bot stops, playing the last frame from its "alert" animation while it is shooting the player. You can use any other frame from any other animation; just make sure that it looks natural.

 

aum56_workshop5

 

       if ((total_frames % 80) == 1)

       {

               vec_for_vertex (temp, my, 8);

               ent_create (bullet_mdl, temp, move_enemy_bullets);

       }

 

Acknex stores the total number of frames that have passed since the game was started in a variable named "total_frames". If we limit the frame rate to 80 fps, just like I did with this demo, and the PC is fast enough, total_frames will be set to 80 after 1 second, to 160 after 2 seconds, and so on - get it?

 

Our "if ((total_frames % 80) == 1)" line of code can be translated to "if total_frames reaches 1, 81, 161, 241...". This means that the two lines of code that are placed inside the curly brackets are executed each and every second, provided that the computer is able to deliver at least 80 fps in our level.

 

The first line of code inside the brackets uses the "vec_for_vertex" instruction, which can get the xyz coordinates for each and every vertex of a model that is placed in our level. We will want to use this instruction if we want to attach a particle effect to the tip of a torch model, or when we need to glue two models together (maybe a player and a sword) in a proper position, and so on.

 

I am using vec_for_vertex here because I want Ackbot to fire the bullets using its pistol, and not the (default) origin of the model. Let's examine the general form of the "vec_for_vertex" instruction:

 

vec_for_vertex (variable, entity, vertex_number);

 

- "variable" is the name of the variable that will store the xyz position of the vertex;

- "entity" is the name of the entity that interests us. We can use "my" here if we want to get the position of a vertex for the current entity, or any other pointer name;

- "vertex_number" is the number of the vertex that interests us.

 

I know that this sounds a bit too technical, so let's take a look at the following picture.

 

aum56_workshop6

 

You can see that the highlighted vertex, the one that interests us (the tip of the gun) is the 8th vertex of the model. Let's see the same piece of code one more time:

 

       if ((total_frames % 80) == 1)

       {

               vec_for_vertex (temp, my, 8);

               ent_create (bullet_mdl, temp, move_enemy_bullets);

       }

 

It's all easier now, right? Vec_for_vertex stores the xyz coordinates of the 8th vertex inside a variable named temp and the following line creates a bullet at the proper  position (temp), attaching it the "move_enemy_bullets" function.

 

       if (vec_dist (player.x, my.x) > 1500)

       {

               my.status = idle;

       }

}

 

These are the last few lines in the first "while" loop that drives our enemy and their role is really simple: if the player has moved away from the enemy (farther than 1500 quants), Ackbot switches back to its "idle" state.

 

The first loop runs for as long as player's status isn't "dead". What happens when the player dies? Take a look at the code below to find out:

 

while (death_percentage < 100)

{

       ent_animate(my, "deatha", death_percentage, null);

       death_percentage += 3 * time;

       wait (1);

}

my.passable = on;

 

We play the "death" animation (named "deatha" for my test model) once with the speed given by "3 * time", and then, at the end of the animation, we make the corpse passable, because the player must be able to pass through the dead enemy. Don't ask me why we have to do that because I don't know the answer; this is how they usually do it in games :).

 

Time to examine enemy's event code:

 

aum56_workshop7

 

function got_shot()

{

       if (you.skill30 != 1) {return;}

       my.health -= 35;

       if (my.health <= 0)

       {

               my.status = dead;

               my.event = null;

               return;

       }

       else

       {

               my.status = attacking;

       }

}

 

Please remember that this function runs each and every time when an entity hits or collides with our enemy. This means that under normal conditions we could hit and kill Ackbot by simply running into it with our player model! We don't want something like this to happen, so I have set skill30 to 1 for each bullet; the first line of code checks if the enemy has collided with an entity ("you", the entity that has triggered the event) that is a bullet or not; if the code doesn't detect a skill30 set to 1, it returns, ignoring the event.

 

If Ackbot is hit by a bullet, its health is decreased by 35; if its health is smaller than or equal to zero, we set its status to "dead", so that the enemy can play its "death" animation. We also set Ackbot's event function to "null", making sure that the bot won't react to events from now on, and then we "return", getting out of the function. If the enemy isn't dead yet, it will change its status to "attacking", starting to fire at the player. The player has a similar, yet simplified event function; I didn't discuss it, but you have got all the needed explanations in this paragraph.

 

Let's look at the last function that needs our attention:

 

function move_enemy_bullets()

{

       var bullet_speed;

       my.skill30 = 1;

       my.enable_impact = on;

       my.enable_entity = on;

       my.enable_block = on;

       my.event = remove_bullets;

       my.pan = you.pan;

       my.tilt = you.tilt;

       bullet_speed.x = 50 * time;

       bullet_speed.y = 0;

       bullet_speed.z = 0;

       while (my != null)

       {

               c_move (my, bullet_speed, nullvector, ignore_you);

               wait (1);

       }

}

 

First of all, I am using similar (not identical) functions for player's bullets and Ackbot's bullets; this would allow me to have different bullet speeds, orientations, and so on. The first line of code defines a local variable which will store the speed of the bullet; the second line sets bullet's skill30 to 1, uniquely identifying it as a bullet.

 

I have decided to make the bullets sensitive to impact, to collision with other entities and to collision with level blocks; their event function is named "remove_bullets". The bullets have the same pan and tilt angles with their creator ("you", the enemy) and their speed is given by "50 * time".

 

The following loop will continue to run for as long as the bullet exists (my != null); its content simply moves the bullet using the bullet_speed variable, ignoring its creator because we don't want our bullet to collide with Ackbot.

 

It's been a long run, but I hope that you have enjoyed it at least as much as I did. At this point we have got an enemy that behaves in a simple, and yet intelligent manner. Next month we will get to make it look and behave even smarter. I'll see you then!