Quake Strogganoff Spotlight: Kibbles 'N Gibs!

Sat, Feb 1, 2020 7-minute read

As covered previously in our little function example, Quake has a basic system for blasting enemies into tiny bits. Let’s take a look at how it’s implemented.

How Quake handles gibs

After an enemy receives a certain amount of damage it runs through a few calls in it’s think.die method. Determines how much damage the entity received and if it’s more than a certain amount, call the following functions: ThrowGib and ThrowHead.

Let’s take a look at the soldier code in soldier.qc, around line 304. Again, this may look different if you’re not using 1.06 recoded but this is more just an example:

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 ();
  }
};

The commented out block is how it was in the original Quake code. As the first comment suggests, we check to see if the damage received has passed a certain amount, in this case -35. A grenade or a rocket would certainly do the job. And if this is the case, play that nice satisfying gibbing sound we’ve all come to love. Then ThrowHead and ThrowGib as again we’ve looked at previously in our functions example, passing in parameters for what gib models to use and self.health as the amount of damage received (for calculating the velocity in which to throw them).

And an important statement to note is return; which will exit the function, otherwise it would continue to execute the statements below.

However, if the health was greater than -35 (but below 0), like -34, the soldier plays his standard death sound and has a 50% chance of calling one death animation or another. This is because random() returns a value between 0 and 1. So if it’s less than 0.5, call army_die1, otherwise call army_cdie1.

NOTE: In 1.06 recoded the commented out gibbing statements have been consolidated into a new function called gibbs() which looks like this:

void(string hd) gibbs =
{
// some code needs to play other sounds - call sound() with the same channel and this will not be heard
	sound(self, CHAN_VOICE, "player/udeath.wav", 1, ATTN_NORM);
	if (hd) ThrowHead (hd, self.health);
	ThrowGib ("progs/gib1.mdl", self.health);
	ThrowGib ("progs/gib2.mdl", self.health);
	ThrowGib ("progs/gib3.mdl", self.health);
};

Gibbs() performs the same functions as the commented out block above but does them in one call and passes a parameter for the head model which is pretty slick.

Let’s take a look at what ThrowGib() and ThrowHead() are doing:

void(string gibname, float dm) ThrowGib =
{
//	local	entity new;

	nspawn = spawn();
	nspawn.origin = self.origin;
	setmodel (nspawn, gibname);
	setsize (nspawn, '0 0 0', '0 0 0');
	nspawn.velocity = VelocityForDamage (dm);
	nspawn.movetype = MOVETYPE_BOUNCE;
//	nspawn.solid = SOLID_NOT;
	nspawn.avelocity_x = random()*600;
	nspawn.avelocity_y = random()*600;
	nspawn.avelocity_z = random()*600;
	nspawn.think = SUB_Remove;
	nspawn.ltime = time;
	nspawn.nextthink = time + 10 + random()*10;
//	nspawn.frame = 0;
//	nspawn.flags = 0;
};

A new entity is created called nspawn and various properties are set, deriving values from parameters passed in such as gibname for setting the model and dm for calculating the VelocityForDamage().

Its .movetype is set to MOVETYPE_BOUNCE, so it, uh, bounces around a bit. And a vector called .avelocity controls rotation.

NOTE: Whenever you use a vector ending with _x, _y or _z you are accessing each axis separately.

Something to keep in mind is that you might think you could write the above lines as:

nspawn.avelocity = 'random()*600 random()*600 random()*600';

But not only does this look awful, it’ll also give you a bad vector error.

However, you could write it as:

nspawn.avelocity = '600 600 600';

Although you won’t get any variation in rotation, which doesn’t look as good.

And finally we have the think functions for our newly created entity.

nspawn.think = SUB_Remove;

A SUB_ can be thought of as a utility function, in this case, the next time the nspawn entity thinks, it’ll remove itself from the world :(

nspawn.nextthink = time + 10 + random()*10;

nextthink is the time it’ll delay before it thinks to remove itself.

Alrighty, now onto ThrowHead, at first glance you might think the ThrowHead function is the same as ThrowGib however the important thing to note is the calls to self which changes the calling entity into a gib itself to get one bonus gib without creating a new entity.

void(string gibname, float dm) ThrowHead =
{
	setmodel (self, gibname);
	self.frame = 0;
	self.nextthink = -1;
	self.movetype = MOVETYPE_BOUNCE;
	self.takedamage = DAMAGE_NO;
	self.solid = SOLID_NOT;
	self.view_ofs = '0 0 8';
	setsize (self, '-16 -16 0', '16 16 56');
	self.velocity = VelocityForDamage (dm);
	self.origin_z = self.origin_z - 24;
	self.flags = self.flags - (self.flags & FL_ONGROUND);
	self.avelocity = crandom() * '0 600 0';
};

And if you notice the self.nextthink = -1; it’ll pretty much just sit around forever.

Extending ThrowGib to add new Gib Types

Overall the above implementation is simple and does the job but what if we wanted to add different types of debris without making a whole bunch of new ThrowGib functions for different materials.

Well, Quake 2 for example added to this:

ThrowGib (self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC);

Notice the added GIB_ORGANIC. We could use this parameter to set different model flags to spawn different effects. But first we should have a way of defining our new gib type and keep the code relatively clean and readable. For this, we’ll use an enumeration.

An enumeration is simply a way of assigning names to constants, starting with 0. So if you add the following code to your [modname]_defs.qc:

enum
{
GIB_NONE,
GIB_FLESH,
GIB_METAL,
GIB_FLAMING
};

We actually get numerical values that look like this:

enum
{
0,
1,
2,
3
};

Which means instead of writing code that looks like this:

ThrowGib ("models/objects/gibs/bone/tris.md2", self.health, 3);

We can use human readable functions that look like this:

ThrowGib ("models/objects/gibs/bone/tris.md2", self.health, GIB_FLAMING);

So, let’s modify ThrowGib to support some new gib types using a switch statement, which will read in the value of our gib type parameter and change or add values accordingly:

void(string gibname, float dm, float GibType) ThrowGib =
{
//	local	entity new;

	nspawn = spawn();
	nspawn.origin = self.origin;
	setmodel (nspawn, gibname);
	setsize (nspawn, '0 0 0', '0 0 0');
	nspawn.velocity = VelocityForDamage (dm);
	nspawn.movetype = MOVETYPE_BOUNCE;
//	nspawn.solid = SOLID_NOT;
	nspawn.avelocity_x = random()*600;
	nspawn.avelocity_y = random()*600;
	nspawn.avelocity_z = random()*600;
	nspawn.think = SUB_Remove;
	nspawn.ltime = time;
	nspawn.nextthink = time + 10 + random()*10;
//	nspawn.frame = 0;
//	nspawn.flags = 0;

switch(GibType)
{
  case GIB_NONE:
    nspawn.modelflags = EF_NOMODELFLAGS;
    break;
  case GIB_FLESH:
    nspawn.modelflags = MF_GIB;
    break;
  case GIB_METAL:
    break;
  case GIB_FLAMING:
    nspawn.modelflags = MF_ROCKET;
    nspawn.avelocity_x = random()*900;
    nspawn.avelocity_y = random()*800;
    nspawn.avelocity_z = random()*700;
    nspawn.velocity = 3.11 * (VelocityForDamage (dm));
    nspawn.touch = SUB_Remove;
    break;
  }
}
};

Choose a case that will assign some additional values to our gib. In particular, under GIB_FLESH we have nspawn.modelflags = MF_GIB; which will add a nice blood trail to models and blood decals everywhere for some extra gruesomeness. Likewise, GIB_FLAMING will have effects similar to a rocket, with a smoke trail and a glow effect which looks nice when called from exploding boxes / barrels.

All we have to do now is assign a type somewhere to test, such as in the gibbs function, by modifying ThrowGib:

void(string hd) gibbs =
{
// some code needs to play other sounds - call sound() with the same channel and this will not be heard
	sound(self, CHAN_VOICE, "player/udeath.wav", 1, ATTN_NORM);
	if (hd) ThrowHead (hd, self.health);
	ThrowGib ("progs/gib1.mdl", self.health, GIB_FLESH);
	ThrowGib ("progs/gib2.mdl", self.health, GIB_FLESH);
	ThrowGib ("progs/gib3.mdl", self.health, GIB_FLESH);
};

Now, because we’ve added to an existing function, we’ll have to update all instances in the code that call ThrowGib to include the new GibType parameter, such as in function prototypes. For example in combat.qc, above the gibbs function change:

void(string gibname, float dm) ThrowGib;

To:

void(string gibname, float dm, float GibType) ThrowGib;

And any other instances of:

ThrowGib ("progs/gib1.mdl", self.health);

To something like:

ThrowGib ("progs/gib1.mdl", self.health, GIB_FLESH);

Or else we’ll get too few parameters warnings.

And that should just about wrap things up for gibs.

Enjoy this random video about gibs:

Happy modding!