In Quake and derivative engines (like Half-Life 1 & 2), it is possible to slide up sloped surfaces without losing much speed. This is a major gameplay component of games like QuakeWorld Team Fortress, Team Fortress Classic, and Fortress Forever, where maintaining momentum from large speed bursts is fundamental.
The obvious assumption would be that this is an intentional feature that uses things like the slope of the surface and the player's velocity to determine when a player is rampsliding, but that is not the case. In fact, in the same way that bunnyhopping was likely an unintentional quirk of the 'air acceleration' code, rampsliding was likely an unintentional quirk of the 'categorize position' code.
In Quake's PM_CatagorizePosition
[sic] function, we see the following code:
if (pmove.velocity[2] > 180)
{
onground = -1;
}
That is, if the player is ever moving up at greater than 180 units (velocity index 2 is the vertical component), then the player is automatically considered 'in the air,' and this overrides all other 'on ground' checks. With this, if a player is colliding with a ramp such that their velocity along the ramp has a large enough vertical component, then they are considered 'in the air', and thus ground friction is simply not applied (specifically, PM_AirMove
is called instead of PM_GroundMove
). This state of being 'in the air' while sliding along an otherwise walkable surface is what is meant by the term 'rampsliding.'
Subsequently, there are two emergent conditions for rampsliding:
And these two conditions also interact with eachother (e.g. you can slide a shallower ramp when you're going faster).
Similar code exists in the Half-Life (GoldSrc) engine:
if (pmove->velocity[2] > 180) // Shooting up really fast. Definitely not on ground.
{
pmove->onground = -1;
}
and in the Half-Life 2 (Source) engine:
// Was on ground, but now suddenly am not
if ( bMovingUpRapidly ||
( bMovingUp && player->GetMoveType() == MOVETYPE_LADDER ) )
{
SetGroundEntity( NULL );
}
It seems like this code is mostly a catch-all fix to resolve any instance where a player is moved by an external force that should push them off the ground, but that doesn't directly alter the player's 'on ground' flag--things like explosions, or trigger_push
brush entities. This is necessary because the 'on ground' and 'in air' states are handled very differently: for example, when on the ground, the player's vertical velocity is set to zero every frame, so things like RPG explosions would otherwise never be able to push a player off the ground.
When a player collides with a surface, the resulting velocity is determined using a function called PM_ClipVelocity
. The following is a simplified version of the ClipVelocity
logic:
float backoff = DotProduct(velocity, surfaceNormal);
for (i=0; i<3; i++)
{
float change = surfaceNormal[i] * backoff;
velocity[i] = velocity[i] - change;
}
Its job is to make velocity parallel to the surface that is being collided with. If the velocity is not already close to parallel, then non-negligible speed loss can result from ClipVelocity
, but once velocity is parallel to the surface, running it through the function again will have no effect. ClipVelocity
therefore is responsible for speed loss when first colliding with a slope, and makes it so that the angle of your velocity entering the ramp matters for how much speed you ultimately maintain, but it does not explain speed loss while rampsliding.
This is where gravity comes into the picture: because you are considered 'in the air' while rampsliding, gravity is applied every frame. This creates a loop that goes like this:
ClipVelocity
to make velocity parallel to the surfaceIn this loop, ClipVelocity
basically serves to redistribute changes in velocity among all of its components.
So, if you are rampsliding on a constant slope, all speed loss is typically due to gravity. If you set gravity to 0, you can rampslide infinitely, and if you set gravity really high, you can only rampslide for a second or two. This makes sense if you think of rampsliding in terms of an object sliding up a completely frictionless slope: the force that will make that object eventually stop and start sliding back down the slope is gravity.
Surfing comes from a separate but related mechanism: if a surface is steep enough, then the player is always considered 'in the air' when colliding with it. The speed gain while surfing comes from two places:
ClipVelocity
described above makes you gain speed from gravity when moving down a slopeAirAccelerate
allows you to gain a bit more horizontal speed (when done right), and to control your position on the slopeIt's pretty remarkable to note that almost every movement technique in games like Team Fortress Classic and Fortress Forever was originally accidental:
Even more remarkable is that this phenomenon is actually somewhat common in games, where unintended mechanics become fundamental to the gameplay as we know it today (see things like mutalisk micro in StarCraft, or k-style in GunZ, or even denying creeps in DotA).
After playing a game with rampsliding for a while, it becomes clear that if you land on a flat surface right before a ramp instead of directly on the ramp, you will often maintain more speed. This is due to how ClipVelocity
works: you can maintain more speed after two calls of ClipVelocity
with smaller angle differentials than after a single call with a larger angle differential.
Note that in the above diagram, the velocity loss that would occur from friction when landing on the ground is not represented (i.e. the diagram is showing 'perfect' execution where you land directly in front of the ramp). In reality, the velocity maintained when landing on flat varies depending on how long you slide on the ground before hitting the ramp, since ground friction will be applied during that time.