Quake Strogganoff Spotlight: Quake Soldiers

Sun, Mar 29, 2020 16-minute read

This will be a look at what goes into Quake’s AI by delving into what makes the Quake soldier tick1. A pretty basic AI that basically runs around with no self preservation and occasionally takes shots at the player.

Before we begin to unravel this beast, it’s important to note that when thinking of AI, NOT to think of it in real-time but rather what’s happening each frame, like a game of chess, just with everyone making a discreet move at the same time, changing the board, analyzing it, then making another move. To the computer, it lives only in that particular moment in time, it’s all it knows. It might be able to make predictions about things but it has no idea what the future holds (although technically you can give it an idea). So as we step through the different functions, try to remember what frame of mind the monster is in.

Frame Definitions

At the top of the file you’ll notice a bunch of $frames. You can technically disregard these and have code that still works if you know the individual frame numbers but these simply give names to frame numbers for easier readability (i.e. $stand1 instead of 0, $death3 instead of 10).

$frame stand1 stand2 stand3 stand4 stand5 stand6 stand7 stand8

$frame death1 death2 death3 death4 death5 death6 death7 death8
$frame death9 death10

$frame deathc1 deathc2 deathc3 deathc4 deathc5 deathc6 deathc7 deathc8
$frame deathc9 deathc10 deathc11

$frame load1 load2 load3 load4 load5 load6 load7 load8 load9 load10 load11

$frame pain1 pain2 pain3 pain4 pain5 pain6

$frame painb1 painb2 painb3 painb4 painb5 painb6 painb7 painb8 painb9 painb10
$frame painb11 painb12 painb13 painb14

$frame painc1 painc2 painc3 painc4 painc5 painc6 painc7 painc8 painc9 painc10
$frame painc11 painc12 painc13

$frame run1 run2 run3 run4 run5 run6 run7 run8

$frame shoot1 shoot2 shoot3 shoot4 shoot5 shoot6 shoot7 shoot8 shoot9

$frame prowl_1 prowl_2 prowl_3 prowl_4 prowl_5 prowl_6 prowl_7 prowl_8
$frame prowl_9 prowl_10 prowl_11 prowl_12 prowl_13 prowl_14 prowl_15 prowl_16
$frame prowl_17 prowl_18 prowl_19 prowl_20 prowl_21 prowl_22 prowl_23 prowl_24

Putting On The Ritz, Monster Definition Function

“Let’s make a monster!” - Dr. Frankenstein

NOTE: This example is once again using this codebase by NumberSix but I’ll still cover what the frame functions are doing as per the original Quake source (as well as many other Quake mods). And since this monster code is running on the server, it will be replicated to clients and thus allow for things like coop. Since single player in Quake is basically just multiplayer with 1 player connected.

Start by opening up soldier.qc and scroll down to the bottom. This is where it all begins, the function definition void() monster_army = is what gets called if there are any entities found in the level called monster_army, it’s the entry point for that entity.

/*QUAKED monster_army (1 0 0) (-16 -16 -24) (16 16 40) Ambush
*/
void() monster_army =
{
	if (deathmatch)
	{
		remove(self);
		return;
	}
	precache_model ("progs/soldier.mdl");
	precache_model ("progs/h_guard.mdl");
	precache_model ("progs/gib1.mdl");
	precache_model ("progs/gib2.mdl");
	precache_model ("progs/gib3.mdl");

	precache_sound ("soldier/death1.wav");
	precache_sound ("soldier/idle.wav");
	precache_sound ("soldier/pain1.wav");
	precache_sound ("soldier/pain2.wav");
	precache_sound ("soldier/sattck1.wav");
	precache_sound ("soldier/sight1.wav");

	precache_sound ("player/udeath.wav");		// gib death

	self.solid = SOLID_SLIDEBOX;
	self.movetype = MOVETYPE_STEP;

	setmodel (self, "progs/soldier.mdl");

	setsize (self, '-16 -16 -24', '16 16 40');
	self.health = 30;

	self.th_stand = army_stand1;
	self.th_walk = army_walk1;
	self.th_run = army_run1;
	self.th_missile = army_atk1;
	self.th_pain = army_pain;
	self.th_die = army_die;

	walkmonster_start;
};
/*QUAKED monster_army (1 0 0) (-16 -16 -24) (16 16 40) Ambush
*/

QUAKED comments are entity definitions used by editors like Radiant when using the .def format. For the most part they are obsolete but do help describe any key/vals or spawnflags this entity has that are available to mappers. The first block (1 0 0) is the color of the entity followed by size, and Ambush is a spawnflag.

if (deathmatch)
{
  remove(self);
  return;
}

This checks whether the cvar deathmatch is true and if so, removes the entity. No monsters getting in the way of a nice honest game of deathmatch. This could also be a good spot to set other checks before spawning like if you were to have, say, a cvar like “thereisnospoon” which would could set a different texture for the enemy to use as like a The Matrix easter egg. Any Deus Ex fans reading this?

After that, any models and sounds used by this monster are precached, this is good practice all around as you might get a bunch of console warnings about fixing your code. Not precaching files can also cause savegame errors and performance issues.

self.solid = SOLID_SLIDEBOX;
self.movetype = MOVETYPE_STEP;

setmodel (self, "progs/soldier.mdl");

setsize (self, '-16 -16 -24', '16 16 40');
self.health = 30;

A few parameters are set on the entity for collision detection, movetype, model, health and collision box size.

Then we get to the interesting bits. Think functions!

self.th_stand = army_stand1;
self.th_walk = army_walk1;
self.th_run = army_run1;
self.th_missile = army_atk1;
self.th_pain = army_pain;
self.th_die = army_die;

These are pretty self explanatory, however th_missile is used for ranged attacks. These point to the main built in functions that get called under the various circumstances for which they’re named. Such as checking distance to an enemy and setting up aim vectors that projectiles can use.

And at the bottom, you’ll see a function called walkmonster_start;, or sometimes walkmonster_start(); depending on your codebase. You can check out what it’s doing in monsters.qc, which is setting more initialization parameters, checking for errors and entering more loops behind the scene to determine how it should behave. In this case being a walking monster, it sets the monster drop to the floor on spawn and checks whether or not it’s targeting a path in which to start patrolling.

Functions

The original frame functions are left in here for comparison, just in case you’re NOT using the framer macro.

Also at the top we see a function prototype for army_fire because as previously covered, army_fire is called BEFORE its function is defined further down the file and the compiler won’t know what this means.

void() army_fire;

Standing Frame Think Function

So, the first think function is th_stand

/*
void()	army_stand1	=[	$stand1,	army_stand2	] {ai_stand();};
void()	army_stand2	=[	$stand2,	army_stand3	] {ai_stand();};
void()	army_stand3	=[	$stand3,	army_stand4	] {ai_stand();};
void()	army_stand4	=[	$stand4,	army_stand5	] {ai_stand();};
void()	army_stand5	=[	$stand5,	army_stand6	] {ai_stand();};
void()	army_stand6	=[	$stand6,	army_stand7	] {ai_stand();};
void()	army_stand7	=[	$stand7,	army_stand8	] {ai_stand();};
void()	army_stand8	=[	$stand8,	army_stand1	] {ai_stand();};
*/

framerloop($stand1, $stand8, army_stand1, 0.1,ai_stand());

This is a simple function for when the soldier is, well, just standing around, looping through some idle frames. We start with army_stand1 as called by self.th.stand. He’s got 8 frames of animation that loop at 10 frames per second.

Each of these frame functions, unrolled, would look like this as regular functions:

void() army_stand1 =
{
  self.frame = $stand1;
  self.think = army_stand2;
	self.nextthink = time + 0.1;
  ai_stand();
}
void() army_stand2 =
{
  self.frame = $stand2;
  self.think = army_stand3;
	self.nextthink = time + 0.1;
  ai_stand();
}

Etc…

Which would be even more tedious than the original frame function macro.

The last frame function:

void()	army_stand8	=[	$stand8,	army_stand1	] {ai_stand();};

Calls army_stand1() as its next think, which will make it loop.

But notice the calls to ai_stand(), let’s take a quick look at that:

/*
=============
ai_stand

The monster is staying in one place for a while, with slight angle turns
=============
*/
void() ai_stand =
{
	if (FindTarget ())
		return;

	if (time > self.pausetime)
	{
		self.th_walk ();
		return;
	}

// change angle slightly

};

ai_stand() in turn checks to see if it finds a target using FindTarget (), and if so will break from it’s current function of standing around looking pretty, and start to engage its target either through self.th.missile or if this particular monster had a melee attack, self.th_melee. Without ai_stand(), he would pretty much just stand there until taking damage.

If you’re so inclined, I’d suggest checking out ai.qc to see what all of these functions are doing as the rabbit hole goes deeper. For now I want to keep things relatively simple and not too overwhelming, especially if you’re new to this whole thing.

Walking Frames Think Function

“I’m Walken HERE!” - Christopher Walken (probably at some point in his life)

Up next we have walking. The soldier, like a lot of Quake’s AI usually only walks when it’s patrolling a path set up by a level designer. Mostly because when you’re engaging in combat, you RUN, not WALK, unless you want to get shot! Or at least that’s what my dad always used to say to me.

/*
void()	army_walk1	=[	$prowl_1,	army_walk2	] {
if (random() < 0.2)
	sound (self, CHAN_VOICE, "soldier/idle.wav", 1, ATTN_IDLE);
ai_walk(1);};
void()	army_walk2	=[	$prowl_2,	army_walk3	] {ai_walk(1);};
void()	army_walk3	=[	$prowl_3,	army_walk4	] {ai_walk(1);};
void()	army_walk4	=[	$prowl_4,	army_walk5	] {ai_walk(1);};
void()	army_walk5	=[	$prowl_5,	army_walk6	] {ai_walk(2);};
void()	army_walk6	=[	$prowl_6,	army_walk7	] {ai_walk(3);};
void()	army_walk7	=[	$prowl_7,	army_walk8	] {ai_walk(4);};
void()	army_walk8	=[	$prowl_8,	army_walk9	] {ai_walk(4);};
void()	army_walk9	=[	$prowl_9,	army_walk10	] {ai_walk(2);};
void()	army_walk10	=[	$prowl_10,	army_walk11	] {ai_walk(2);};
void()	army_walk11	=[	$prowl_11,	army_walk12	] {ai_walk(2);};
void()	army_walk12	=[	$prowl_12,	army_walk13	] {ai_walk(1);};
void()	army_walk13	=[	$prowl_13,	army_walk14	] {ai_walk(0);};
void()	army_walk14	=[	$prowl_14,	army_walk15	] {ai_walk(1);};
void()	army_walk15	=[	$prowl_15,	army_walk16	] {ai_walk(1);};
void()	army_walk16	=[	$prowl_16,	army_walk17	] {ai_walk(1);};
void()	army_walk17	=[	$prowl_17,	army_walk18	] {ai_walk(3);};
void()	army_walk18	=[	$prowl_18,	army_walk19	] {ai_walk(3);};
void()	army_walk19	=[	$prowl_19,	army_walk20	] {ai_walk(3);};
void()	army_walk20	=[	$prowl_20,	army_walk21	] {ai_walk(3);};
void()	army_walk21	=[	$prowl_21,	army_walk22	] {ai_walk(2);};
void()	army_walk22	=[	$prowl_22,	army_walk23	] {ai_walk(1);};
void()	army_walk23	=[	$prowl_23,	army_walk24	] {ai_walk(1);};
void()	army_walk24	=[	$prowl_24,	army_walk1	] {ai_walk(1);};
*/

Whoa, what’s going on with that first frame? Let’s unravel that into something more readable:

void()	army_walk1	=[	$prowl_1,	army_walk2	]
{
if (random() < 0.2)
  sound (self, CHAN_VOICE, "soldier/idle.wav", 1, ATTN_IDLE);

  ai_walk(1);
};

NOTE: Usually an if statement would need curly brackets, but QuakeC shortcuts this if there’s only one function call.

So, with 24 frames at 10 frames per second, every time we hit the first frame at 2.4 seconds, the soldier will make an idle sound but only if random() (a random number between 0 and 1) is less than 0.2. Otherwise he’d be quite noisy, but consistent.

The rest looks similar to ai_stand but you’ll notice that ai_walk() passes a value. This is the distance to travel for that that particular frame. Some frames he moves ahead a little more than others, and sometimes not at all. This helps to sync his movement to the animation and avoid looking like he’s skating.

NOTE: In some games and engines, like Half-Life or Doom 3, the movement is derived from the animation itself. In Unreal Engine 4, this is called root motion movement.

And below we see the framer macro at work, making short work of all those frame function calls, frame_arb stands for arbitrary function calls. ai_rep takes in 8 frames at a time, and you’ll notice that they coincide with the ai_walk() movement values above. Like ai_stand(), ai_walk() also checks for FindTarget () which will break from the walk and enter combat.

framerloop($prowl_1, $prowl_24, army_walk1, 0.1,

	frame_arb($prowl_1, if (random() < 0.2) sound (self, CHAN_VOICE, "soldier/idle.wav", 1, ATTN_IDLE));

	ai_rep($prowl_1, ai_walk, 1, 1, 1, 1, 2, 3, 4, 4);
	ai_rep($prowl_9, ai_walk, 2, 2, 2, 1, 0, 1, 1, 1);
	ai_rep($prowl_17, ai_walk, 3, 3, 3, 3, 2, 1, 1, 1);
);

Running Frames Think Function

“Let’s go fuckin’ run like we’re LIONS! ‘n TIGERS! ‘n BEARS!” - Jordan Belfort (Wolf of Wall Street 2013)

/*
void()	army_run1	=[	$run1,		army_run2	] {
if (random() < 0.2)
	sound (self, CHAN_VOICE, "soldier/idle.wav", 1, ATTN_IDLE);
ai_run(11);};
void()	army_run2	=[	$run2,		army_run3	] {ai_run(15);};
void()	army_run3	=[	$run3,		army_run4	] {ai_run(10);};
void()	army_run4	=[	$run4,		army_run5	] {ai_run(10);};
void()	army_run5	=[	$run5,		army_run6	] {ai_run(8);};
void()	army_run6	=[	$run6,		army_run7	] {ai_run(15);};
void()	army_run7	=[	$run7,		army_run8	] {ai_run(10);};
void()	army_run8	=[	$run8,		army_run1	] {ai_run(8);};

*/

framerloop($run1, $run8, army_run1, 0.1,

	frame_arb($run1,if (random() < 0.2) sound (self, CHAN_VOICE, "soldier/idle.wav", 1, ATTN_IDLE));
	ai_rep($run1, ai_run, 11, 15, 10, 10, 8, 15, 10, 8);
);

Attack (missile) Frames Think Function

/*
void()	army_atk1	=[	$shoot1,	army_atk2	] {ai_face();};
void()	army_atk2	=[	$shoot2,	army_atk3	] {ai_face();};
void()	army_atk3	=[	$shoot3,	army_atk4	] {ai_face();};
void()	army_atk4	=[	$shoot4,	army_atk5	] {ai_face();};
void()	army_atk5	=[	$shoot5,	army_atk6	] {ai_face();army_fire();
self.effects = self.effects | EF_MUZZLEFLASH;};
void()	army_atk6	=[	$shoot6,	army_atk7	] {ai_face();};
void()	army_atk7	=[	$shoot7,	army_atk8	] {ai_face();SUB_CheckRefire (army_atk1);};
void()	army_atk8	=[	$shoot8,	army_atk9	] {ai_face();};
void()	army_atk9	=[	$shoot9,	army_run1	] {ai_face();};
*/

framer($shoot1, $shoot9, army_atk1, army_run1, 0.1,
	ai_face();
	frame_arb($shoot5,
		army_fire();
		self.effects = self.effects | EF_MUZZLEFLASH;
	);
	frame_arb($shoot7,SUB_CheckRefire (army_atk1));
);

Pain Frame Functions

/*
void()	army_pain1	=[	$pain1,		army_pain2	] {};
void()	army_pain2	=[	$pain2,		army_pain3	] {};
void()	army_pain3	=[	$pain3,		army_pain4	] {};
void()	army_pain4	=[	$pain4,		army_pain5	] {};
void()	army_pain5	=[	$pain5,		army_pain6	] {};
void()	army_pain6	=[	$pain6,		army_run1	] {ai_pain(1);};
*/

framer($pain1, $pain6, army_pain1, army_run1, 0.1,frame_arb($pain6,ai_pain(1)));

Once again, the monster plays some animations frames and at the end calls army_run1 so it doesn’t just stand around after getting hurt and ai_pain(1); moves the monster back by a distance of 1 (or at least it does in the recoded codebase).

/*
void()	army_painb1	=[	$painb1,	army_painb2	] {};
void()	army_painb2	=[	$painb2,	army_painb3	] {ai_painforward(13);};
void()	army_painb3	=[	$painb3,	army_painb4	] {ai_painforward(9);};
void()	army_painb4	=[	$painb4,	army_painb5	] {};
void()	army_painb5	=[	$painb5,	army_painb6	] {};
void()	army_painb6	=[	$painb6,	army_painb7	] {};
void()	army_painb7	=[	$painb7,	army_painb8	] {};
void()	army_painb8	=[	$painb8,	army_painb9	] {};
void()	army_painb9	=[	$painb9,	army_painb10] {};
void()	army_painb10=[	$painb10,	army_painb11] {};
void()	army_painb11=[	$painb11,	army_painb12] {};
void()	army_painb12=[	$painb12,	army_painb13] {ai_pain(2);};
void()	army_painb13=[	$painb13,	army_painb14] {};
void()	army_painb14=[	$painb14,	army_run1	] {};
*/

framer($painb1, $painb14, army_painb1, army_run1, 0.1,

	frame_arb($painb2,ai_painforward(13));
	frame_arb($painb3,ai_painforward(9));
	frame_arb($painb12,ai_pain(2));

);

Same as army_pain1(), this is just a variant with more frames and some code to either move the monster forward or back depending on the frame.

/*
void()	army_painc1	=[	$painc1,	army_painc2	] {};
void()	army_painc2	=[	$painc2,	army_painc3	] {ai_pain(1);};
void()	army_painc3	=[	$painc3,	army_painc4	] {};
void()	army_painc4	=[	$painc4,	army_painc5	] {};
void()	army_painc5	=[	$painc5,	army_painc6	] {ai_painforward(1);};
void()	army_painc6	=[	$painc6,	army_painc7	] {ai_painforward(1);};
void()	army_painc7	=[	$painc7,	army_painc8	] {};
void()	army_painc8	=[	$painc8,	army_painc9	] {ai_pain(1);};
void()	army_painc9	=[	$painc9,	army_painc10] {ai_painforward(4);};
void()	army_painc10=[	$painc10,	army_painc11] {ai_painforward(3);};
void()	army_painc11=[	$painc11,	army_painc12] {ai_painforward(6);};
void()	army_painc12=[	$painc12,	army_painc13] {ai_painforward(8);};
void()	army_painc13=[	$painc13,	army_run1] {};
*/

framer($painc1, $painc13, army_painc1, army_run1, 0.1,

	frametwo_arb($painc2,$painc8,ai_pain(1));
	ai_rep($painc5, ai_painforward, 1, 1, -6, -6, 4, 3, 6, 8);
);

Starting to see a pattern here? How about the difference between the original frames and the framer replacement? Hmmm, interesting.

Pain Think Function

void(entity attacker, float damage)	army_pain =
{
//	local float r;

	if (self.pain_finished > time)
		return;

	self.rnd_ = random();

	if (self.rnd_ < 0.2)
	{
		self.pain_finished = time + 0.6;
		army_pain1 ();
		sound (self, CHAN_VOICE, "soldier/pain1.wav", 1, ATTN_NORM);
	}
	else if (self.rnd_ < 0.6)
	{
		self.pain_finished = time + 1.1;
		army_painb1 ();
		sound (self, CHAN_VOICE, "soldier/pain2.wav", 1, ATTN_NORM);
	}
	else
	{
		self.pain_finished = time + 1.1;
		army_painc1 ();
		sound (self, CHAN_VOICE, "soldier/pain2.wav", 1, ATTN_NORM);
	}
};

Time to bring on the pain. When a monster receives damage it thinks it’s th_pain think function and because we like a bit of variety in our monsters getting hurt, this will select a random animation to play. The solder has 3 to choose from.

You’ll notice:

if (self.pain_finished > time)
  return;

This will exit the function if pain_finished is less than time preventing any other pain animations from being called until the soldier is done playing his pain animations.. Later on in the function you’ll see statements like self.pain_finished = time + 0.6; before calling army_pain1 ();. We can see that army_pain1 has 6 frames of animations, and so that would be 0.1 per frame for a total of 0.6.

Ranged Attack Function

void() army_fire =
{
//	local	vector	dir;
//	local	entity	en;

	ai_face();

	sound (self, CHAN_WEAPON, "soldier/sattck1.wav", 1, ATTN_NORM);

// fire somewhat behind the player, so a dodging player is harder to hit
//	en = self.enemy;

	self.v__ = self.enemy.origin - self.enemy.velocity*0.2;
	self.v__ = normalize (self.v__ - self.origin);

	FireBullets (4, self.v__, '0.1 0.1 0');
};

So, here is the function definition for army_fire() for which we saw the prototype at the top of the file. This cause the soldier to face their target ai_face();, plays a sound() effect, takes aim at its enemy using some vector math as the comment suggests (to aims slightly behind) what their most likely target is (the player)

There’s a more detailed description about what some of the vector functions are doing (like normalize) here by Legion. Excerpt below:

“The function normalize converts a vector of a certain length to the vector of length one. A vector has two components: the direction and the magnitude. Velocity, for instance, is a vector. A velocity has a direction and a magnitude we often call speed. The normalize function, then, changes this vector in such a way so that only its magnitude changes but not its direction.”

And the final function call blasts a shotgun using FireBullets(float shotcount, vector dir, vector spread) which is an interesting function that allows for a number of pellets or shots to be combined into a single damage function.

Death Frames

Just like how the soldier handles pain, there are multiple death animations to choose from. During each animation, the entity is set to SOLID_NOT so players no longer collide with it and then drop some loot with DropBackPack(), which takes its loot (in this case some ammo_shells), which is set to 5. You can see what DropBackpack() is doing in items.qc.

/*
void()	army_die1	=[	$death1,	army_die2	] {};
void()	army_die2	=[	$death2,	army_die3	] {};
void()	army_die3	=[	$death3,	army_die4	]
{self.solid = SOLID_NOT;self.ammo_shells = 5;DropBackpack();};
void()	army_die4	=[	$death4,	army_die5	] {};
void()	army_die5	=[	$death5,	army_die6	] {};
void()	army_die6	=[	$death6,	army_die7	] {};
void()	army_die7	=[	$death7,	army_die8	] {};
void()	army_die8	=[	$death8,	army_die9	] {};
void()	army_die9	=[	$death9,	army_die10	] {};
void()	army_die10	=[	$death10,	army_die10	] {};
*/

framer($death1, $death10, army_die1, SUB_Null, 0.1,

	frame_arb($death3,

		self.solid = SOLID_NOT;
		self.ammo_shells = 5;
		DropBackpack();
	);
);
/*
void()	army_cdie1	=[	$deathc1,	army_cdie2	] {};
void()	army_cdie2	=[	$deathc2,	army_cdie3	] {ai_back(5);};
void()	army_cdie3	=[	$deathc3,	army_cdie4	]
{self.solid = SOLID_NOT;self.ammo_shells = 5;DropBackpack();ai_back(4);};
void()	army_cdie4	=[	$deathc4,	army_cdie5	] {ai_back(13);};
void()	army_cdie5	=[	$deathc5,	army_cdie6	] {ai_back(3);};
void()	army_cdie6	=[	$deathc6,	army_cdie7	] {ai_back(4);};
void()	army_cdie7	=[	$deathc7,	army_cdie8	] {};
void()	army_cdie8	=[	$deathc8,	army_cdie9	] {};
void()	army_cdie9	=[	$deathc9,	army_cdie10	] {};
void()	army_cdie10	=[	$deathc10,	army_cdie11	] {};
void()	army_cdie11	=[	$deathc11,	army_cdie11	] {};
*/

framer($deathc1, $deathc11, army_cdie1, SUB_Null, 0.1,

	frame_arb($deathc3,

		self.solid = SOLID_NOT;
		self.ammo_shells = 5;
		DropBackpack();
	);
	ai_rep($deathc2, ai_back, 5, 4, 13, 3, 4, -6, -6, -6);
);

Death Think Function

Here we see, similar to the pain animation selection (and covered previously in this “series”), this monster checks to see what its health is when below 0, chooses whether to gib or not and then selects one of two death animations.

void() army_die =
{
// check for gib
	if (self.health < -35)
	{
/*
		sound (self, CHAN_VOICE, "player/udeath.wav", 1, ATTN_NORM);
		ThrowHead ("progs/h_guard.mdl", self.health);
		ThrowGib ("progs/gib1.mdl", self.health);
		ThrowGib ("progs/gib2.mdl", self.health);
		ThrowGib ("progs/gib3.mdl", self.health);
*/
		gibbs("progs/h_guard.mdl");
		return;
	}

// regular death
	sound (self, CHAN_VOICE, "soldier/death1.wav", 1, ATTN_NORM);
	if (random() < 0.5)
		army_die1 ();
	else
		army_cdie1 ();
};

And that about wraps this little spotlight up. Hopefully this has helped to make the AI in Quake a bit more understandable.

For additional and invaluable AI tutorials, I highly recommend checking out the excellent resources at AI Cafe.

Happy modding!


  1. Albeit half-assed? ↩︎