Rampsliding Is a Quake Engine Quirk in the Same Way That Bunnyhopping Is

2019-05-27

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 creates two emergent conditions for rampsliding:

The ramp can’t be too shallow

The player can’t be going too slow

And these two conditions also interact with eachother (e.g. you can slide a shallower ramp when you’re going faster).

30° 700 606 350 player state: 'in air'

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

Why would this code exist?

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.

If there’s no friction, why do you slow down while rampsliding?

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.

30° 495 700 velocity maintained during ClipVelocity: 71%

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:

Try to move along a surface

ClipVelocity to make velocity parallel to the surface

to make velocity parallel to the surface Move along the surface using the adjusted velocity

Apply gravity by subtracting from the vertical component of velocity, making the velocity no longer parallel to the surface

Repeat

In this loop, ClipVelocity basically serves to redistribute changes in velocity among all of its components.

30° 700 606 350 Clip Velocity

Move

Apply Gravity Gravity (per loop):

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.

What about surfing (like in Counter-Strike surf maps)?

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:

The same interaction with ClipVelocity described above makes you gain speed from gravity when moving down a slope

described above makes you gain speed from gravity when moving down a slope AirAccelerate allows you to gain a bit more horizontal speed (when done right), and to control your position on the slope

Wrapping up

It’s pretty remarkable to note that almost every movement technique in games like Team Fortress Classic and Fortress Forever was originally accidental:

Bunnyhopping was an unintentional feature spawned from how acceleration was implemented

Rampsliding was an unintentional feature spawned from a fix for a completely unrelated ‘stuck on the ground’ bug

Concussion grenades were intended to displace/disorient the enemy team, but they got used to boost yourself instead and form the basis of TFC/FF’s high-speed offense

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).

Addendum: Landing in front of a ramp instead of directly on it

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.

30° 350 700 30° 525 606 700 increased % velocity maintained from landing on flat: 50%

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.