On this page
  1. Related
  2. Games
  3. References
  4. Sources

Bunny hopping chains jumps so the player is almost never on the ground, where friction slows them, while air strafing adds speed in the air. Quake checks its jump after friction, so each hop costs one frame of friction, about 5.6 % per hop at 72 frames/s1 at 72 frames a second1. QuakeWorld checks the jump first, so a hop on the first ground frame takes no friction2. Quake III Arena also checks the jump before friction, so its hops lose nothing to the floor either3.

  • Landing frictionQuake (1996). Friction runs before the jump in each frame, so every hop spends one frame under ground friction, which QuakeWorld's movement code skips.1,2
  • FrictionlessQuake II (1997). The jump is checked before friction, so a hop on the first ground step keeps its speed, but a hard landing can lock the jump for 18 or 25 timer units.4Quake III Arena (1999). The jump is checked before friction, so a hop on the first ground step keeps all its speed, as in QuakeWorld.3

Games

GameYearHowNote
Quake1996Landing frictionfriction runs before the jump in each frame, so every hop spends one frame under ground friction, which QuakeWorld's movement code skips
Quake II1997Frictionlessthe jump is checked before friction, so a hop on the first ground step keeps its speed, but a hard landing can lock the jump for 18 or 25 timer units
Quake III Arena1999Frictionlessthe jump is checked before friction, so a hop on the first ground step keeps all its speed, as in QuakeWorld
Every claim about this mechanic, by game (4)
Quake 1996Because Quake runs friction in SV_ClientThink before the QuakeC jump in PlayerPreThink, and the on-ground flag is set by the walking move when it ends on a floor, a jump can come no sooner than the frame after landing, and that frame takes ground friction first: computed, a hop loses 4/72 of its horizontal speed (5.6%) at 72 frames a second above 100 units/s, or 8/72 (11.1%) when no floor lies within 34 units below a point 16 units ahead.Code
QuakeWorld's pmove.c checks the jump before friction: PlayerMove calls JumpButton, which clears onground, and then PM_Friction, whose ground friction runs only while onground is set, so a QuakeWorld hop on its first ground frame loses no speed to friction, where WinQuake's takes one frame of it (quake/hop-friction). Both still need the button released between jumps, and both add 270.Code
Quake II 1997Quake II checks the jump before friction: Pmove calls PM_CheckJump, which clears the ground entity, and then PM_Friction, whose ground friction needs a ground entity, so a hop on the first command that starts on the ground loses no speed to friction. QuakeWorld's PlayerMove has the same order: JumpButton clears onground before PM_Friction, whose ground friction needs it. Two conditions apply. The jump key must be released and pressed again (the jump claim). And when the position check first finds ground with the vertical speed below −200, it sets a landing timer of 18 units, or 25 below −400, that blocks the jump until it runs out; each command lowers it by its length in ms divided by 8, rounded down and at least 1, so 18 units last 144 ms at 8 or 16 ms commands and 180 ms at 10 ms (computed). By the atlas's reading the slide move clips the velocity along a floor it runs into, and the stepping move keeps that clipped vertical speed, so a landing step that reaches the floor would not set the timer; how often a hop's landing sets it has not been measured.Code
Quake III Arena 1999Quake III checks the jump before friction: each step, PmoveSingle traces for ground before the move, PM_WalkMove calls PM_CheckJump before PM_Friction, and a jump clears the walking flag and hands the step to PM_AirMove, whose friction call skips ground friction without that flag. So a hop on the first step that starts on the ground loses no speed to friction, as in QuakeWorld (quake/qw-hop-friction), where WinQuake's takes one frame of it, 5.6% at 72 frames a second (quake/hop-friction). The hop needs the jump key released and pressed again in the air, as in both.Code

References

  1. ^ abcBecause Quake runs friction in SV_ClientThink before the QuakeC jump in PlayerPreThink, and the on-ground flag is set by the walking move when it ends on a floor, a jump can come no sooner than the frame after landing, and that frame takes ground friction first: computed, a hop loses 4/72 of its horizontal speed (5.6%) at 72 frames a second above 100 units/s, or 8/72 (11.1%) when no floor lies within 34 units below a point 16 units ahead.Codeid-Software/Quake · id-Software/Quake-Tools
    How we know
    612	SV_RunClients ();
    ⋮
    617		SV_Physics ();
    
    WinQuake/host.c lines 612–617 at bf4ac42 · upstream ↗
    387	onground = (int)sv_player->v.flags & FL_ONGROUND;
    
    WinQuake/sv_user.c line 387 at bf4ac42 · upstream ↗
    361	else if ( onground )
    ⋮
    363		SV_UserFriction ();
    364		SV_Accelerate ();
    
    WinQuake/sv_user.c lines 361–364 at bf4ac42 · upstream ↗
    1069	PR_ExecuteProgram (pr_global_struct->PlayerPreThink);
    
    WinQuake/sv_phys.c line 1069 at bf4ac42 · upstream ↗
    970	oldonground = (int)ent->v.flags & FL_ONGROUND;
    971	ent->v.flags = (int)ent->v.flags & ~FL_ONGROUND;
    
    WinQuake/sv_phys.c lines 970–971 at bf4ac42 · upstream ↗
    284			if (trace.ent->v.solid == SOLID_BSP)
    ⋮
    286				ent->v.flags =	(int)ent->v.flags | FL_ONGROUND;
    
    WinQuake/sv_phys.c lines 284–287 at bf4ac42 · upstream ↗
    1033	if (downtrace.plane.normal[2] > 0.7)
    ⋮
    1035		if (ent->v.solid == SOLID_BSP)
    ⋮
    1037			ent->v.flags =	(int)ent->v.flags | FL_ONGROUND;
    
    WinQuake/sv_phys.c lines 1033–1037 at bf4ac42 · upstream ↗
    798	if (!(self.flags & FL_ONGROUND))
    799		return;
    
    qcc/v101qc/client.qc lines 798–799 at c0d1b91 · upstream ↗
    144	if (trace.fraction == 1.0)
    145		friction = sv_friction.value*sv_edgefriction.value;
    ⋮
    151	newspeed = speed - host_frametime*control*friction;
    
    WinQuake/sv_user.c lines 144–151 at bf4ac42 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  2. ^ abQuakeWorld's pmove.c checks the jump before friction: PlayerMove calls JumpButton, which clears onground, and then PM_Friction, whose ground friction runs only while onground is set, so a QuakeWorld hop on its first ground frame loses no speed to friction, where WinQuake's takes one frame of it (quake/hop-friction). Both still need the button released between jumps, and both add 270.Codeid-Software/Quake
    How we know
    889	if (pmove.cmd.buttons & BUTTON_JUMP)
    890		JumpButton ();
    891	else
    892		pmove.oldbuttons &= ~BUTTON_JUMP;
    ⋮
    894	PM_Friction ();
    
    QW/client/pmove.c lines 889–894 at bf4ac42 · upstream ↗
    674	if (onground == -1)
    675		return;		// in air, so no effect
    ⋮
    677	if ( pmove.oldbuttons & BUTTON_JUMP )
    678		return;		// don't pogo stick
    ⋮
    680	onground = -1;
    681	pmove.velocity[2] += 270;
    
    QW/client/pmove.c lines 674–681 at bf4ac42 · upstream ↗
    366	else if (onground != -1) // apply ground friction
    ⋮
    368		control = speed < movevars.stopspeed ? movevars.stopspeed : speed;
    369		drop += control*friction*frametime;
    
    QW/client/pmove.c lines 366–369 at bf4ac42 · upstream ↗
    1460	PlayerMove ();
    
    QW/server/sv_user.c line 1460 at bf4ac42 · upstream ↗
    361	else if ( onground )
    ⋮
    363		SV_UserFriction ();
    364		SV_Accelerate ();
    
    WinQuake/sv_user.c lines 361–364 at bf4ac42 · upstream ↗
    1069	PR_ExecuteProgram (pr_global_struct->PlayerPreThink);
    
    WinQuake/sv_phys.c line 1069 at bf4ac42 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  3. ^ abQuake III checks the jump before friction: each step, PmoveSingle traces for ground before the move, PM_WalkMove calls PM_CheckJump before PM_Friction, and a jump clears the walking flag and hands the step to PM_AirMove, whose friction call skips ground friction without that flag. So a hop on the first step that starts on the ground loses no speed to friction, as in QuakeWorld (quake/qw-hop-friction), where WinQuake's takes one frame of it, 5.6% at 72 frames a second (quake/hop-friction). The hop needs the jump key released and pressed again in the air, as in both.Codeid-Software/Quake-III-Arena · id-Software/Quake · id-Software/Quake-Toolsrevised
    How we know
    1962	// set groundentity
    1963	PM_GroundTrace();
    
    code/game/bg_pmove.c lines 1962–1963 at dbe4ddb · upstream ↗
    1165	pml.groundPlane = qtrue;
    1166	pml.walking = qtrue;
    
    code/game/bg_pmove.c lines 1165–1166 at dbe4ddb · upstream ↗
    710	if ( PM_CheckJump () ) {
    711		// jumped away
    712		if ( pm->waterlevel > 1 ) {
    713			PM_WaterMove();
    714		} else {
    715			PM_AirMove();
    ⋮
    717		return;
    ⋮
    720	PM_Friction ();
    
    code/game/bg_pmove.c lines 710–720 at dbe4ddb · upstream ↗
    377	pml.walking = qfalse;
    
    code/game/bg_pmove.c line 377 at dbe4ddb · upstream ↗
    610	PM_Friction();
    
    code/game/bg_pmove.c line 610 at dbe4ddb · upstream ↗
    196	if ( pm->waterlevel <= 1 ) {
    197		if ( pml.walking && !(pml.groundTrace.surfaceFlags & SURF_SLICK) ) {
    ⋮
    200				control = speed < pm_stopspeed ? pm_stopspeed : speed;
    201				drop += control*pm_friction*pml.frametime;
    
    code/game/bg_pmove.c lines 196–201 at dbe4ddb · upstream ↗
    1916	if ( pm->cmd.upmove < 10 ) {
    1917		// not holding jump
    1918		pm->ps->pm_flags &= ~PMF_JUMP_HELD;
    
    code/game/bg_pmove.c lines 1916–1918 at dbe4ddb · upstream ↗
    889	if (pmove.cmd.buttons & BUTTON_JUMP)
    890		JumpButton ();
    891	else
    892		pmove.oldbuttons &= ~BUTTON_JUMP;
    ⋮
    894	PM_Friction ();
    
    QW/client/pmove.c lines 889–894 at bf4ac42 · upstream ↗
    674	if (onground == -1)
    675		return;		// in air, so no effect
    ⋮
    677	if ( pmove.oldbuttons & BUTTON_JUMP )
    678		return;		// don't pogo stick
    ⋮
    680	onground = -1;
    ⋮
    683	pmove.oldbuttons |= BUTTON_JUMP;	// don't jump again until released
    
    QW/client/pmove.c lines 674–683 at bf4ac42 · upstream ↗
    366	else if (onground != -1) // apply ground friction
    ⋮
    368		control = speed < movevars.stopspeed ? movevars.stopspeed : speed;
    369		drop += control*friction*frametime;
    
    QW/client/pmove.c lines 366–369 at bf4ac42 · upstream ↗
    972	if (self.button2)
    ⋮
    974		PlayerJump ();
    ⋮
    976	else
    977		self.flags = self.flags | FL_JUMPRELEASED;
    
    qcc/v101qc/client.qc lines 972–977 at c0d1b91 · upstream ↗
    799		return;
    ⋮
    801	if ( !(self.flags & FL_JUMPRELEASED) )
    802		return;		// don't pogo stick
    
    qcc/v101qc/client.qc lines 798–811 at c0d1b91 · upstream ↗
    361	else if ( onground )
    ⋮
    363		SV_UserFriction ();
    364		SV_Accelerate ();
    
    WinQuake/sv_user.c lines 361–364 at bf4ac42 · upstream ↗
    1069	PR_ExecuteProgram (pr_global_struct->PlayerPreThink);
    
    WinQuake/sv_phys.c line 1069 at bf4ac42 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Revised after a check by a second AI model, 2026-10-22.

    In the claim ledger

  4. ^Quake II checks the jump before friction: Pmove calls PM_CheckJump, which clears the ground entity, and then PM_Friction, whose ground friction needs a ground entity, so a hop on the first command that starts on the ground loses no speed to friction. QuakeWorld's PlayerMove has the same order: JumpButton clears onground before PM_Friction, whose ground friction needs it. Two conditions apply. The jump key must be released and pressed again (the jump claim). And when the position check first finds ground with the vertical speed below −200, it sets a landing timer of 18 units, or 25 below −400, that blocks the jump until it runs out; each command lowers it by its length in ms divided by 8, rounded down and at least 1, so 18 units last 144 ms at 8 or 16 ms commands and 180 ms at 10 ms (computed). By the atlas's reading the slide move clips the velocity along a floor it runs into, and the stepping move keeps that clipped vertical speed, so a landing step that reaches the floor would not set the timer; how often a hop's landing sets it has not been measured.Codeid-Software/Quake-2 · id-Software/Quakerevised
    How we know
    1333	else
    ⋮
    1335		PM_CheckJump ();
    ⋮
    1337		PM_Friction ();
    
    qcommon/pmove.c lines 1333–1337 at 372afde · upstream ↗
    819	pm->groundentity = NULL;
    
    qcommon/pmove.c line 819 at 372afde · upstream ↗
    365	if ((pm->groundentity && pml.groundsurface && !(pml.groundsurface->flags & SURF_SLICK) ) || (pml.ladder) )
    
    qcommon/pmove.c line 365 at 372afde · upstream ↗
    714			if (! (pm->s.pm_flags & PMF_ON_GROUND) )
    715			{	// just hit the ground
    716				pm->s.pm_flags |= PMF_ON_GROUND;
    717				// don't do landing time if we were just going down a slope
    718				if (pml.velocity[2] < -200)
    ⋮
    720					pm->s.pm_flags |= PMF_TIME_LAND;
    721					// don't allow another jump for a little while
    722					if (pml.velocity[2] < -400)
    723						pm->s.pm_time = 25;	
    724					else
    725						pm->s.pm_time = 18;
    
    qcommon/pmove.c lines 714–726 at 372afde · upstream ↗
    1302	// drop timing counter
    1303	if (pm->s.pm_time)
    ⋮
    1305		int		msec;
    ⋮
    1307		msec = pm->cmd.msec >> 3;
    1308		if (!msec)
    1309			msec = 1;
    1310		if ( msec >= pm->s.pm_time) 
    ⋮
    1312			pm->s.pm_flags &= ~(PMF_TIME_WATERJUMP | PMF_TIME_LAND | PMF_TIME_TELEPORT);
    1313			pm->s.pm_time = 0;
    ⋮
    1315		else
    1316			pm->s.pm_time -= msec;
    
    qcommon/pmove.c lines 1302–1316 at 372afde · upstream ↗
    685	point[2] = pml.origin[2] - 0.25;
    ⋮
    693		trace = pm->trace (pml.origin, pm->mins, pm->maxs, point);
    
    qcommon/pmove.c lines 683–693 at 372afde · upstream ↗
    219		for (i=0 ; i<numplanes ; i++)
    ⋮
    221			PM_ClipVelocity (pml.velocity, planes[i], pml.velocity, 1.01);
    
    qcommon/pmove.c lines 219–221 at 372afde · upstream ↗
    332	//!! Special case
    333	// if we were walking along a plane, then we need to copy the Z over
    334	pml.velocity[2] = down_v[2];
    
    qcommon/pmove.c lines 332–334 at 372afde · upstream ↗
    889	if (pmove.cmd.buttons & BUTTON_JUMP)
    890		JumpButton ();
    891	else
    892		pmove.oldbuttons &= ~BUTTON_JUMP;
    ⋮
    894	PM_Friction ();
    
    QW/client/pmove.c lines 889–894 at bf4ac42 · upstream ↗
    680	onground = -1;
    
    QW/client/pmove.c line 680 at bf4ac42 · upstream ↗
    366	else if (onground != -1) // apply ground friction
    ⋮
    368		control = speed < movevars.stopspeed ? movevars.stopspeed : speed;
    369		drop += control*friction*frametime;
    
    QW/client/pmove.c lines 366–369 at bf4ac42 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Revised after a check by a second AI model, 2026-10-22.

    In the claim ledger

Sources

Code