Bunny hopping
Jumping again the moment a jump lands, so the player spends as few frames as possible under ground friction while air strafing builds speed.
On this page
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
| Game | Year | How | Note |
|---|---|---|---|
| Quake | 1996 | Landing friction | friction runs before the jump in each frame, so every hop spends one frame under ground friction, which QuakeWorld's movement code skips |
| Quake II | 1997 | Frictionless | 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 |
| Quake III Arena | 1999 | Frictionless | the 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)
References
- ^ abcBecause Quake runs friction in
SV_ClientThinkbefore the QuakeC jump inPlayerPreThink, 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-ToolsHow 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.
- ^ abQuakeWorld's
pmove.cchecks the jump before friction:PlayerMovecallsJumpButton, which clearsonground, and thenPM_Friction, whose ground friction runs only whileongroundis 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/QuakeHow 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.
- ^ abQuake III checks the jump before friction: each step,
PmoveSingletraces for ground before the move,PM_WalkMovecallsPM_CheckJumpbeforePM_Friction, and a jump clears the walking flag and hands the step toPM_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-ToolsrevisedHow 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.
- ^Quake II checks the jump before friction:
PmovecallsPM_CheckJump, which clears the ground entity, and thenPM_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'sPlayerMovehas the same order:JumpButtonclearsongroundbeforePM_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/QuakerevisedHow 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.
Sources
Code
- Software, id. 2012. “Quake: the engine source release (id-Software/Quake: WinQuake and QuakeWorld).” Commit bf4ac42. id-Software/Quake @ bf4ac42 · licence, all uses
- Software, id. 2012. “Quake: the QuakeC source and compiler (id-Software/Quake-Tools, qcc/v101qc).” Commit c0d1b91. id-Software/Quake-Tools @ c0d1b91 · licence, all uses
- Software, id. 2012. “Quake III Arena: the GPL source release (id-Software/Quake-III-Arena, 1.32b).” Commit dbe4ddb. id-Software/Quake-III-Arena @ dbe4ddb · licence, all uses