Back to Top

RPG Tutorials

Working with AI-based enemies

tutorial5

Artificial Intelligence has become a hot topic these days, but game programmers have been struggling to code intelligent enemies since the beginning of time :)


 

This tutorial will teach you how to code an FSM (finite-state machine) enemy. It's not going to be a perfect one (the code would be too complex for a beginner) but it will work great for most people's needs, chasing the player all over the map. Let's get started!

Enemies and blood go together like... copy and paste ;) so let's take a quick look at a particle-based blood effect.


 

function fade_blood(PARTICLE *p)

{

p.alpha -= 5 * time_step;

if (p.alpha < 0)

p.lifespan = 0;

}


function particle_blood(PARTICLE *p)

{

p->vel_x = random(2) - 1;

p->vel_y = random(2) - 1;

p->vel_z = random(1) - 1.5;

p.alpha = 70 + random(30);

p.bmap = blood_pcx;

p.size = 1 + random(2);

p.flags |= (BRIGHT | MOVE | TRANSLUCENT);

p.lifespan = 20;

p.event = fade_blood;

}


 

That's it! I told you it's going to be a quick look, because this particle effect is very similar with the one that was used for the fire effect, which was discussed a while ago. Just note the negative vel_z value, because blood drops are supposed to move towards the ground, right?


 

action t_rpg_skeleton()

{

c_setminmax (me);

vec_scale(my.min_x, 0.6);

vec_scale(my.max_x, 0.6);

VECTOR skeleton_speed, sword_tip, sword_base, temp, trace_temp;

my.health = 100;

while (!player) {wait (1);}

my.skill80 = 0;

my.emask |= (ENABLE_SHOOT | ENABLE_SCAN);

my.event = damage_skeleton;


 

The action above is attached to the skeleton model that's displayed in the picture at the top of the screen. The first line of code sets the collision detection hull to the actual size of the model; without this line, the skeleton would have a hard time trying to get really close to the player, because it has a thick bounding box. In fact, the second and third lines of code have a similar role, reducing the bounding box of the 3D model to 60%.


 

Then, we define some vectors, we set the health value for our skeleton to 100 points, and then we wait until the player model is loaded in the level. The entity's skill80 will be reset (it's going to be used for animation) and the skeleton is made sensitive to shoots and scans that are performed by other entities (mostly the player). If one of these events are triggered, the damage_skeleton function will be run.


 

while (vec_dist (my.x, player.x) >= 2000)

{

ent_animate(my, "stand", my.skill80, ANM_CYCLE);

my.skill80 += 4 * time_step;

wait (1);

}

while (my.health > 0)

{

if ((vec_dist (my.x, player.x) < 2000) && (player.life > 0))

{

vec_set(temp, player.x);

vec_sub(temp, my.x);

vec_to_angle(my.pan, temp);

skeleton_speed.x = 20 * time_step;

skeleton_speed.y = 0;

vec_set (trace_temp, my.x);

trace_temp.z -= 10000;

skeleton_speed.z = -c_trace (my.x, trace_temp.x, IGNORE_ME | IGNORE_PASSABLE | IGNORE_MODELS | USE_BOX) - 1;

c_move (my, skeleton_speed, nullvector, IGNORE_PASSABLE | IGNORE_PASSENTS | GLIDE);

ent_animate(my, "walk", my.skill89, ANM_CYCLE);

my.tilt = 0;

my.skill89 += 7 * time_step;

my.skill89 %= 100;


 

The skeleton will wait until the player comes closer than 2,000 units to it, playing its "stand" animation. The next loop ensures that the skeleton attacks, moves, etc. only while it is alive. If the player isn't dead and has come closer than 2,000 units to the skeleton, the code will tell the enemy to rotate towards the player, and then move towards it.


 

The c_trace instruction ensures that the skeleton doesn't float up in the air, while c_move will move the enemy in the direction of its pan angle, i.e. towards the player. The last four lines of code will loop the "walk" animation and ensure that the model is straight (its tilt angle is zero).


 

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

{

my.skill80 = 0;

snd_play (growl_attack_wav, 30, 0);

while ((my.skill80 < 100) && (player.life > 0))

{

vec_for_vertex (sword_tip, my, 291);

vec_for_vertex (sword_base, my, 306);

c_trace (sword_base, sword_tip, IGNORE_ME | IGNORE_PASSABLE);

if (result != 0)

{

effect (particle_blood, 2, target, normal);

if (you == player)

{

player.life -= 2 * time_step;

}

ent_playsound (my, sword_wav, 50);

}

ent_animate(my, "attack", my.skill80, ANM_CYCLE);

my.skill80 += 5 * time_step; // "attack" animation speed

wait (1);

}

wait (-0.1);

}

}


 

If the distance between the skeleton and the player is under 150 units, a sound effect will be played, and an attack will be triggered. The loop will only run for as long as the player is alive; the skeleton shouldn't attack player's corpse, right? To make sure that the sword attack is real and effective, we c_trace between the enemy's sword base and tip vertices. For my 3D skeleton model, those would be the 291st and 306th vertices.


 

If the c_trace instruction hits the player, we trigger the particle blood effect and we reduce player's health. A 3D sword_wav sound effect is played, along with an "attack" animation.


 

else

{

ent_animate(my, "stand", my.skill80, ANM_CYCLE);

my.skill80 += 2 * time_step;

my.skill80 %= 100;

}

wait (1);

}


 

The code above runs when the player has managed to move away from the skeleton, or when it is dead. If this is the case, the skeleton will play its "stand" animation.


 

my.skill80 = 0;

while (my.skill80 < 80)

{

ent_animate(my, "death", my.skill80, ANM_CYCLE);

my.skill80 += 1 * time_step;

wait (1);

}

set (my, PASSABLE);

}


If the enemy is dead, its "death" animation will be played, and then its corpse will be made passable.


function damage_skeleton()

{

var skeleton_hit;

if (event_type == EVENT_SHOOT || event_type == EVENT_SCAN)

{

SND_CREATE_STATIC(skeleton_sound, trpg_pain_wav);

my.skill97 += 1;

if (my.skill97 % 1000 == 1)

skeleton_hit = snd_play(skeleton_sound, 100, 0);

my.health -= (3 + player.attack / 100) * time_step;

wait (1);

}

}


 

The last function reduces the enemy's health each time it is hit by the player. It does that by monitoring the "shoot" and "scan" events, which are triggered either by "c_trace" or "c_scan". When it is attacked, the enemy will lose 3 health points each frame, for as long as the attack continues.


 

This concludes our RPG enemy tutorial. I may write a more advanced AI guide in the future, if there are many requests for one. It will be based on A* so the code won't be very pretty; I'll do my best to make it as easy to use as possible, though.