Quake Strogganoff Lesson: Materials System (Footsteps)

Sat, Feb 8, 2020 9-minute read

I’ve always been a fan of games that treat different materials accordingly, metal should make ricochet sounds and softer materials like wood should spawn splinters and as we’re going to cover here, footsteps.

NOTE: You’ll need to procure your own additional footstep sounds.

Alternatively, there’s the “Quake 3 method” which stores the surface type in the BSP as will be the focus of this tutorial which will make use of Q3BSP maps only. Also should work with FBSP/RBSP if you’re using FTEQW.

Adding footsteps

So, we’re going to start with a function that checks whether or not it should trace from the player, read the surface information and play a footstep sound at a certain interval.

Let’s put the footstep and material related stuff in one spot, I’ve made a new .qc file called materialsys_surface.qc and added it to the progs.src right after stogganoff_defs.qc.

First let’s set up some a function that will consider the conditions on whether a footstep sound should play, like the velocity of the entity or if it’s dead or in the air or even in noclip mode.

NOTE: Some of these values (such as velocity) would be great for some #define’s in your modname_defs.

float CheckFootStep () =
{
	if (self.deadflag)
		return FALSE;
	if (!(self.flags & FL_ONGROUND))
		return FALSE;
	if (vlen(self.velocity) < 100)
		return FALSE;
	if (self.movetype == MOVETYPE_NOCLIP || self.movetype == MOVETYPE_FLY)
		return FALSE;
  	if (self.waterlevel >= 3)
	{
		return FALSE;
	}

	return TRUE;
}

Now with the validator function out of the way we can add the code that will cycle through a few random samples with each step and the speed at which they should play.

void() Q3Surface_FootStep =
{
	local float r;

	r = random() * 4;

	if (!CheckFootStep())
	{
		return;
	}

	if (time < self.nextfootstep)
	return;

	if (vlen(self.velocity) < 200)
		self.nextfootstep = time + 0.35 + random() * 0.1;
	else
		self.nextfootstep = time + 0.25 + random() * 0.1;

	if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_NOSTEPS)
	{
		return;
	}
	else if (self.waterlevel == 1) //Foot Splashes
	{
		if (r < 1) sound(self, CHAN_FOOT, "player/splash1.wav", 0.5, ATTN_IDLE);
		else if (r < 2) sound(self, CHAN_FOOT, "player/splash2.wav", 0.5, ATTN_IDLE);
		else if (r < 3) sound(self, CHAN_FOOT, "player/splash3.wav", 0.5, ATTN_IDLE);
		else            sound(self, CHAN_FOOT, "player/splash4.wav", 0.5, ATTN_IDLE);
	}
	else if (self.waterlevel == 2) //Water Wade
	{
		if (r < 1) sound(self, CHAN_FOOT, "player/wade1.wav", 0.5, ATTN_IDLE);
		else if (r < 2) sound(self, CHAN_FOOT, "player/wade2.wav", 0.5, ATTN_IDLE);
		else if (r < 3) sound(self, CHAN_FOOT, "player/wade3.wav", 0.5, ATTN_IDLE);
		else            sound(self, CHAN_FOOT, "player/wade2.wav", 0.5, ATTN_IDLE);
	}
	else
	{
		if (r < 1)
		{
			sound(self, CHAN_FOOT, "player/foot1.wav", 0.5, ATTN_IDLE);
		}
		else if (r < 2)
		{
			sound(self, CHAN_FOOT, "player/foot2.wav", 0.5, ATTN_IDLE);
		}
		else if (r < 3)
		{
			sound(self, CHAN_FOOT, "player/foot3.wav", 0.5, ATTN_IDLE);
		}
		else
		{
			sound(self, CHAN_FOOT, "player/foot4.wav", 0.5, ATTN_IDLE);
		}
	}
};

And we’ll call our footstep function in client.qc at the end of void() PlayerPostThink in client.qc, just below CheckPowerups ();:

The repeated r = random() * 4; calls aren’t necessary but remain in place in case new sounds are even added.

...

	CheckPowerups ();

	Q3Surface_FootStep();
};

The above code also adds a few things if you have the sound effects for it by checking the water level the player is in and plays some splashing, or wading sounds. Also checks the velocity and either plays at a faster or slower rate. If not, you can comment out or remove the code but I thought I’d leave it in for anyone who might find it useful. Also, note that this is using CHAN_FOOT which I believe is also supported by FTEQW.

NOTE: Don’t forget to precache any new sounds in world.qc under void() worldspawn = just above the lightstyles stuff.

precache_sound ("player/foot1.wav");
precache_sound ("player/foot2.wav");
precache_sound ("player/foot3.wav");
precache_sound ("player/foot4.wav");

If all went to well we should have some footstep sounds playing when walking or running around.

Adding Q3SURFACEFLAG_METALSTEPS

So, you may have noticed the only actual Q3 surface flag we used was Q3SURFACEFLAG_NOSTEPS which returned control from the function. Let’s add some metal. For this we’ll need a custom shader to define our new surface type within the compiled map. If we check dpdefs/extentions we can see that there’s already a metal surfacetype as used in Quake3: Arena.

In your strogganoff/scripts folder, make a new file called strogganoff.shader in this file we will pour all of our cruetly, our malice to rule them all and, er, no just test shaders.

Add strogganoff.shader to shaderlist.txt.

NOTE: This is used by Q3Map2 for reading parms and doing fancy stuff during compilation.

Edit your strogganoff.shader to look something like this:

textures/test/test_metal
{
	surfaceparm metalsteps
 	{
   		map $lightmap
   		rgbGen identity
   		tcGen lightmap
 	}
 	{
   		map textures/somefolder/somemetaltexture.tga
   		rgbGen identity
   		blendFunc GL_DST_COLOR GL_ZERO
 	}
}

More information about shaders in DarkPlaces

For a list of supported Q3BSP surface flags, check out DP_TRACE_HITCONTENTSMASK_SURFACEINFO in dpextentions.qc:

//DP_TRACE_HITCONTENTSMASK_SURFACEINFO
//idea: LadyHavoc
//darkplaces implementation: LadyHavoc
//globals:
.float dphitcontentsmask; // if non-zero on the entity passed to traceline/tracebox/tracetoss this will
//override the normal collidable contents rules and instead hit these contents values
//(for example AI can use tracelines that hit DONOTENTER if it wants to,
//by simply changing this field on the entity passed to traceline),
//this affects normal movement as well as trace calls
float trace_dpstartcontents; // DPCONTENTS_ value at start position of trace
float trace_dphitcontents; // DPCONTENTS_ value of impacted surface
//(not contents at impact point, just contents of the surface that was hit)
float trace_dphitq3surfaceflags; // Q3SURFACEFLAG_ value of impacted surface
string trace_dphittexturename; // texture name of impacted surface
//constants:
float DPCONTENTS_SOLID = 1; // hit a bmodel, not a bounding box
float DPCONTENTS_WATER = 2;
float DPCONTENTS_SLIME = 4;
float DPCONTENTS_LAVA = 8;
float DPCONTENTS_SKY = 16;
float DPCONTENTS_BODY = 32; // hit a bounding box, not a bmodel
float DPCONTENTS_CORPSE = 64; // hit a SOLID_CORPSE entity
float DPCONTENTS_NODROP = 128; // an area where backpacks should not spawn
float DPCONTENTS_PLAYERCLIP = 256; // blocks player movement
float DPCONTENTS_MONSTERCLIP = 512; // blocks monster movement
float DPCONTENTS_DONOTENTER = 1024; // AI hint brush
float DPCONTENTS_LIQUIDSMASK = 14; // WATER | SLIME | LAVA
float DPCONTENTS_BOTCLIP = 2048; // AI hint brush
float DPCONTENTS_OPAQUE = 4096; // only fully opaque brushes get this (may be useful for line of sight checks)
float Q3SURFACEFLAG_NODAMAGE = 1;
float Q3SURFACEFLAG_SLICK = 2; // low friction surface
float Q3SURFACEFLAG_SKY = 4; // sky surface (also has NOIMPACT and NOMARKS set)
float Q3SURFACEFLAG_LADDER = 8; // climbable surface
float Q3SURFACEFLAG_NOIMPACT = 16; // projectiles should remove themselves on impact (this is set on sky)
float Q3SURFACEFLAG_NOMARKS = 32; // projectiles should not leave marks, such as decals (this is set on sky)
float Q3SURFACEFLAG_FLESH = 64; // projectiles should do a fleshy effect (blood?) on impact
float Q3SURFACEFLAG_NODRAW = 128; // compiler hint (not important to qc)
//float Q3SURFACEFLAG_HINT = 256; // compiler hint (not important to qc)
//float Q3SURFACEFLAG_SKIP = 512; // compiler hint (not important to qc)
//float Q3SURFACEFLAG_NOLIGHTMAP = 1024; // compiler hint (not important to qc)
//float Q3SURFACEFLAG_POINTLIGHT = 2048; // compiler hint (not important to qc)
float Q3SURFACEFLAG_METALSTEPS = 4096; // walking on this surface should make metal step sounds
float Q3SURFACEFLAG_NOSTEPS = 8192; // walking on this surface should not make footstep sounds
float Q3SURFACEFLAG_NONSOLID = 16384; // compiler hint (not important to qc)
//float Q3SURFACEFLAG_LIGHTFILTER = 32768; // compiler hint (not important to qc)
//float Q3SURFACEFLAG_ALPHASHADOW = 65536; // compiler hint (not important to qc)
//float Q3SURFACEFLAG_NODLIGHT = 131072; // compiler hint (not important to qc)
//float Q3SURFACEFLAG_DUST = 262144; // translucent 'light beam' effect (not important to qc)
//description:
//adds additional information after a traceline/tracebox/tracetoss call.
//also (very important) sets trace_* globals before calling .touch functions,
//this allows them to inspect the nature of the collision (for example
//determining if a projectile hit sky), clears trace_* variables for the other
//object in a touch event (that is to say, a projectile moving will see the
//trace results in its .touch function, but the player it hit will see very
//little information in the trace_ variables as it was not moving at the time)

Looks like Q3SURFACEFLAG_METALSTEPS is just what we want. Bargain!

So back in our materialsys_surface.qc right after the water level checks, add in the METALSTEPS sounds:

void() Q3Surface_FootStep =
{

...

	else if (self.waterlevel == 2) //Water Wade
	{
		if (r < 1) sound(self, CHAN_FOOT, "player/wade1.wav", 0.5, ATTN_IDLE);
		else if (r < 2) sound(self, CHAN_FOOT, "player/wade2.wav", 0.5, ATTN_IDLE);
		else if (r < 3) sound(self, CHAN_FOOT, "player/wade3.wav", 0.5, ATTN_IDLE);
		else            sound(self, CHAN_FOOT, "player/wade2.wav", 0.5, ATTN_IDLE);
	}
        else if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_METALSTEPS)
	{
		if (r < 1) sound(self, CHAN_FOOT, "player/clank1.wav", 0.5, ATTN_IDLE);
		else if (r < 2) sound(self, CHAN_FOOT, "player/clank2.wav", 0.5, ATTN_IDLE);
		else if (r < 3) sound(self, CHAN_FOOT, "player/clank3.wav", 0.5, ATTN_IDLE);
		else sound(self, CHAN_FOOT, "player/clank4.wav", 0.5, ATTN_IDLE);
	}
	else
	{
		if (r < 1)
		{
			sound(self, CHAN_FOOT, "player/foot1.wav", 0.5, ATTN_IDLE);
		}

...

};

Adding new surface types

How about adding some wood?

So, to add our new surface type we check out double the previous value of 262144 for Q3SURFACEFLAG_DUST and we get 524288, which we can add to our strogganoff_defs.qc

//Additional surface flags

float Q3SURFACEFLAG_WOOD = 524288;

Now, we just need to let the tools know about our new surface flags. Luckily Q3MAP2 makes this really easy with custinfoparms.txt.

NOTE: When you compile, make sure to add -custinfoparms or select it under BSP in Q3Map2Build.

Edit or create a file called custinfoparms.txt in the same folder as Q3Map2 and put the following into it:

{
}
{
        wood 0x80000
}

The first block is for custom contents while the second is for surfaces.

NOTE: One important thing to note is that Q3Map2 reads these values in HEX. When we convert 524288 to HEX, we get 0x80000.

And now back to our footstep code, let’s add a check for wood just under metal in footsteps.qc:

void() Q3Surface_FootStep =
{

...
	else if (trace_dphitq3surfaceflags & Q3SURFACEFLAG_WOOD)
	{
		r = random() * 4;
		if (r < 1) sound(self, CHAN_FOOT, "player/wood1.wav", 0.5, ATTN_IDLE);
		else if (r < 2) sound(self, CHAN_FOOT, "player/wood2.wav", 0.5, ATTN_IDLE);
		else if (r < 3) sound(self, CHAN_FOOT, "player/wood3.wav", 0.5, ATTN_IDLE);
		else            sound(self, CHAN_FOOT, "player/wood4.wav", 0.5, ATTN_IDLE);
	}

...

};

And we’ll also need a custom shader, this time with the keyword “wood” instead of metal. Let’s just change our metal one for now to test.

textures/test/test_metal
{
	surfaceparm wood
 	{
   		map $lightmap
   		rgbGen identity
   		tcGen lightmap
 	}
 	{
   		map textures/somefolder/somemetaltexture.tga
   		rgbGen identity
   		blendFunc GL_DST_COLOR GL_ZERO
 	}
}

And that’s basically the process for adding new surface types and footsteps using Q3BSP.

Happy Modding!