Surface detection and Footsteps in Quake Engine
Footstep sound is very important for the atmosphere of the game, they must fit the surrounding of the game, cracky squeaky noises on wood, splashing water when walking on wet dirt, metal echo when climbing down the metal ramp. All these details create the needed atmosphere for the game. Today it is a basic function of the game and without such details it is hard to imagine any success of the game. In order to implement this we must check the surface our character is standing on. The original Quake had no surface detection. Or rather, there is, but only those built into the engine, hardcoded. These are, for example, triggers, water surfaces, and things like that; there are very few of them. Therefore, even in mods, when they create footsteps sounds, they are the same for any surface.
That is why most of the Quake engines extend engine functionality to add different surface checks.
There are few ways we can implement surface check, we can check for texture names or we can check material surfaceparms.
Let's take a look at extensions file:
//DP_QC_GETSURFACE
//idea: LadyHavoc
//darkplaces implementation: LadyHavoc
//builtin definitions:
float(entity e, float s) getsurfacenumpoints = #434;
vector(entity e, float s, float n) getsurfacepoint = #435;
vector(entity e, float s) getsurfacenormal = #436;
string(entity e, float s) getsurfacetexture = #437;
float(entity e, vector p) getsurfacenearpoint = #438;
vector(entity e, float s, vector p) getsurfaceclippedpoint = #439;Also this:
//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)
Pay attention to comments. The engine will autofill trace_dpstartcontents, trace_dphitcontents, trace_dphitq3surfaceflags, trace_dphittexturename if they are defined every time you cast a traceline and it hits something. This means we can use a traceline (or tracebox) to figure out what material we are standing on. This gives us a really simple algorithm we may utilize in our game. If a player moves on the ground, every N seconds we trace down from the player position to check the material conditions. We may look for the material name or material flag (such as Q3SURFACEFLAG_METALSTEPS) to play a corresponding sound. Comparing texture names to a defined string is way more simpler than defining surface flags in shader files. Also the amount of q3map flags available is very limited. Look at the following code snippet. I also should check if the playing is swimming or walking on the water.
void PlayerFootsteps()
{
if(vlen(self.velocity) < 60) return;
if(!(self.flags & FL_ONGROUND)) return;
if(self.crouching) return;
if(self.movetype != MOVETYPE_WALK) return;
if(time + random(0, 0.1) < self.footstep_finished) return;
float r = random();
if(!FOOTSTEPS_CHECK_SURFACE) // || clienttype(self) != CLIENTTYPE_REAL
{
PlayRandomFootstepSound("footsteps/footstep1.wav", "footsteps/footstep2.wav", "footsteps/footstep3.wav", "footsteps/footstep4.wav");
}
else
{
traceline(self.origin, self.origin - '0 0 64', TRUE, self);
if (self.flags & FL_INWATER && self.waterlevel > 0)
{
sound(self, CHAN_BODY, "misc/h2ohit1.wav", 1, SoundRadius(550));
}
else if(StringContains(trace_dphittexturename, "metal"))
{
PlayRandomFootstepSound("footsteps/metal1.wav", "footsteps/metal2.wav", "footsteps/metal3.wav", "footsteps/metal4.wav");
}
else if(StringContains(trace_dphittexturename, "dirt"))
{
PlayRandomFootstepSound("footsteps/gravel1.wav", "footsteps/gravel2.wav", "footsteps/gravel3.wav", "footsteps/gravel4.wav");
}
else
{
PlayRandomFootstepSound("footsteps/footstep1.wav", "footsteps/footstep2.wav", "footsteps/footstep3.wav", "footsteps/footstep4.wav");
}
}
self.footstep_finished = time + FOOTSTEPS_INTERVAL;
if(self.button4) self.footstep_finished = time + FOOTSTEPS_INTERVAL_RUN;
}
There is an older, alternative way to get the texture name using getsurfacepoint.
float surfnum = getsurfacenearpoint(world, e.origin - '0 0 24');
if (surfnum >= 0)
{
s = getsurfacetexture(world, surfnum);
if(strstrofs(s,"metal", 0) >= 0)
//Play metal
}
}Surface flags
If you want to implement a flag system you need to define metalsteps (or other flags) in a shader file. Inside scripts folder create a .shader file with the following content:
textures/metal/metal01
{
surfaceparm metalsteps
{
map textures/metal/metal01
rgbGen identity
}
{
map $lightmap
rgbGen identity
blendFunc filter
}
}This is a simple shader with metalsteps surface flag.
How in our function we can compare trace_dphitq3surfaceflags to the defined flags. Here is a list of available flags:
//surfaceparm slick // 2
//surfaceparm dust //262144
//surfaceparm metalsteps //4096
//surfaceparm noimpact //16
//surfaceparm nomarks //32
//surfaceparm alphashadow //65536
//surfaceparm hint //256
//surfaceparm lightfilter //32768In our function we may compare it like that:
if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_METALSTEPS)As you can see we don't have enough default surface flags to define all game-physics materials. In that case we may remap default unused flags or we may define our surfaceparms.
Custom surfaceparms
These surface parameters are defined by custinfoparms.txt (a script file that defines a custom surfaceparms to be used with surfaceparm keyword in materials) and used to assign game-related properties to material, for example, hit effect and step sound. This file should be created in the Gamedir/scripts directory.
Example custinfoparms.txt:
// Custom Infoparms File
// Custom Contentsflags
{
interactclip 0x00000080
weaponclip 0x00000100
bossclip 0x00000200
}
// Custom Surfaceflags
{
generic 0x00000000
dirt 0x00000040
grass 0x00008040
gravel 0x00018000
grate 0x00020000
metal 0x00020040
ice 0x00028000
mud 0x00028040
puddle 0x00040000
soft 0x00040040
snow 0x00048000
stone 0x00048040
wood 0x00060000
flesh 0x00068000
}
Integers are HEX values in 0x00000000 format representing surfaceflags and contentflags to be assigned with corresponding custom surfaceparm. Q3MAP2 will use this file if you run it with -custinfoparms. Now you can compare trace_dphitq3surfaceflags value with HEX (you can convert it to decimal) values above to check the surface. In a similar way we can detect surfaces for other events such as bullet impacts, interaction, etc.
Tags: darkplaces, q3map, shader
Comments