Strafe-running
Combining forward movement with a strafe can exceed the forward speed, depending on how the game caps movement.
Strafe-running is movement that gains speed by combining forward and sideways input. Games add movement axes independently, cap the combined ground wish, or gain speed through air acceleration, so the same diagonal input has different effects across games1,2,3.
#Separately capped movement
Wolfenstein 3D adds the strafe as a perpendicular thrust and caps it separately from forward movement, so a keyboard diagonal run moves 1.41 times1 the forward speed1. Doom also caps its forward and side moves separately, but its keyboard values make a forward run with a strafe 1.28 ×4 as fast as a forward run4. With Doom’s strafe modifier, a turn key and a strafe key can both add to the side move until it reaches the forward cap; players call that SR505.
Duke Nukem 3D caps forward and sideways movement separately, so its diagonal keyboard run is 1.41 ×6 the forward speed6. A turn key and a strafe key together can reach the side cap and add more diagonal speed6. Descent 3 extends separately capped thrust to the ship’s forward, vertical, and sideways axes; all three pushes sum to 1.73 ×7 the forward thrust, with the same ratio at steady speed under the source’s linear drag7.
#Ground caps and air acceleration
Quake caps the combined ground wish speed at 3202, so strafing raises a walk from 2002 to the cap but does not raise a run already at the cap2. In the air, Quake applies a strong push along the wish direction while that component of velocity remains below 30 units/s3,3. Aiming the wish sideways maximizes the computed gain8.
Quake II caps the combined ground wish speed at 3009, which removes the diagonal’s extra speed at a run9. By default its air acceleration is weak and stops when velocity along the wish direction reaches the full wish speed; a deathmatch server can enable Quake’s stronger 30 units/s3 rule10.
Quake III Arena scales combined ground input so forward, strafe, and diagonal commands each wish 320 units/s11 at a run11. Its weak air acceleration stops at the full wish speed along the wish direction12. The computed speed gain is greatest when the wish is angled into the motion, and the best angle widens as speed rises13.
- Separately capped axesWolfenstein 3D (1992). With the strafe button held the side move is a second thrust, capped apart from the forward one, so a diagonal keyboard run is 1.41 times as fast.1Doom (1993). Forward and side moves are capped apart, so a diagonal run is 1.28 times as fast, and 1.41 at a side move of 50.4,5Duke Nukem 3D (1996). Forward and side moves are capped apart at 90, so a diagonal keyboard run is 1.41 times as fast.6Descent 3 (1999). Forward, up, and sideways are capped apart and summed, so all three at once push 1.73 times as hard as forward alone.7
- Air strafeQuake (1996). On the ground a strafe only helps a walk; in the air the push along the wish direction stops at 30, so turning into the strafe keeps adding speed.2,3,8,14
- Angled air strafeQuake II (1997). On the ground the 300 cap takes away a strafe's gain at a run; in the air the push is weak but capped only at the full wish speed, unless a deathmatch server switches on Quake's rule.9,10Quake III Arena (1999). On the ground the diagonal is scaled back to 320; in the air the push is weak but capped only at the full wish speed, so the gain is biggest with the wish a few degrees off the motion.11,12,13
Games
| Game | Year | How | Note |
|---|---|---|---|
| Wolfenstein 3D | 1992 | Separately capped axes | with the strafe button held the side move is a second thrust, capped apart from the forward one, so a diagonal keyboard run is 1.41 times as fast |
| Doom | 1993 | Separately capped axes | forward and side moves are capped apart, so a diagonal run is 1.28 times as fast, and 1.41 at a side move of 50 |
| Duke Nukem 3D | 1996 | Separately capped axes | forward and side moves are capped apart at 90, so a diagonal keyboard run is 1.41 times as fast |
| Descent 3 | 1999 | Separately capped axes | forward, up, and sideways are capped apart and summed, so all three at once push 1.73 times as hard as forward alone |
| Quake | 1996 | Air strafe | on the ground a strafe only helps a walk; in the air the push along the wish direction stops at 30, so turning into the strafe keeps adding speed |
| Quake II | 1997 | Angled air strafe | on the ground the 300 cap takes away a strafe's gain at a run; in the air the push is weak but capped only at the full wish speed, unless a deathmatch server switches on Quake's rule |
| Quake III Arena | 1999 | Angled air strafe | on the ground the diagonal is scaled back to 320; in the air the push is weak but capped only at the full wish speed, so the gain is biggest with the wish a few degrees off the motion |
Every claim about this mechanic, by game (14)
References
- ^ abcdWith the strafe button held, Wolfenstein 3D's side move is a second
Thrustat 90° to the facing, andPollControlscaps the side and forward control values separately at 100 a tic, so running forward and sideways on the keyboard (70 each) moves √2 ≈ 1.41 times as fast as running forward alone, about 14,849 units a tic (computed).Codeid-Software/wolf3dHow we know
164 if (buttonstate[bt_strafe]) ⋮ 167 // strafing ⋮ 170 if (controlx > 0) ⋮ 172 angle = ob->angle - ANGLES/4; 173 if (angle < 0) 174 angle += ANGLES; 175 Thrust (angle,controlx*MOVESCALE); // move to left ⋮ 177 else if (controlx < 0) ⋮ 179 angle = ob->angle + ANGLES/4; 180 if (angle >= ANGLES) 181 angle -= ANGLES; 182 Thrust (angle,-controlx*MOVESCALE); // move to right
WOLFSRC/WL_AGENT.C lines 164–183 at 0516778 · upstream ↗ 205 if (controly < 0) ⋮ 207 Thrust (ob->angle,-controly*MOVESCALE); // move forwards
WOLFSRC/WL_AGENT.C lines 205–207 at 0516778 · upstream ↗ 343 if (buttonstate[bt_run]) ⋮ 345 if (Keyboard[dirscan[di_north]]) 346 controly -= RUNMOVE*tics; 347 if (Keyboard[dirscan[di_south]]) 348 controly += RUNMOVE*tics; 349 if (Keyboard[dirscan[di_west]]) 350 controlx -= RUNMOVE*tics; 351 if (Keyboard[dirscan[di_east]]) 352 controlx += RUNMOVE*tics;
WOLFSRC/WL_PLAY.C lines 343–353 at 0516778 · upstream ↗ 540 max = 100*tics; 541 min = -max; 542 if (controlx > max) 543 controlx = max; 544 else if (controlx < min) 545 controlx = min; ⋮ 547 if (controly > max) 548 controly = max;
WOLFSRC/WL_PLAY.C lines 540–548 at 0516778 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcdeQuake's keyboard moves are
cl_forwardspeedandcl_backspeed200 andcl_sidespeed350, doubled by the run key (cl_movespeedkey2.0), and the menu's "always run" sets forward and back to 400. Computed from these and the 320 cap: walking forward wishes 200, while a strafe alone (350), a walk with a strafe (√(200² + 350²) ≈ 403), and any run all wish 320.Codeid-Software/QuakeHow we know
213cvar_t cl_forwardspeed = {"cl_forwardspeed","200", true}; 214cvar_t cl_backspeed = {"cl_backspeed","200", true}; 215cvar_t cl_sidespeed = {"cl_sidespeed","350"}; ⋮ 217cvar_t cl_movespeedkey = {"cl_movespeedkey","2.0"};
WinQuake/cl_input.c lines 213–217 at bf4ac42 · upstream ↗ 298 cmd->sidemove += cl_sidespeed.value * CL_KeyState (&in_moveright); ⋮ 306 cmd->forwardmove += cl_forwardspeed.value * CL_KeyState (&in_forward); ⋮ 313 if (in_speed.state & 1) ⋮ 315 cmd->forwardmove *= cl_movespeedkey.value; 316 cmd->sidemove *= cl_movespeedkey.value;
WinQuake/cl_input.c lines 298–317 at bf4ac42 · upstream ↗ 1115 case 8: // allways run ⋮ 1123 Cvar_SetValue ("cl_forwardspeed", 400); 1124 Cvar_SetValue ("cl_backspeed", 400);
WinQuake/menu.c lines 1115–1124 at bf4ac42 · upstream ↗ 351 if (wishspeed > sv_maxspeed.value) ⋮ 353 VectorScale (wishvel, sv_maxspeed.value/wishspeed, wishvel); 354 wishspeed = sv_maxspeed.value;
WinQuake/sv_user.c lines 351–354 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.
- ^ abcdeIn the air, Quake's
SV_AirAcceleratetakes the wish velocity's direction and length, caps that length at 30, and adds speed along the wish direction only while the velocity's component along it is under 30: the push issv_accelerate× the uncapped wish speed (up to 320) × frame time, cut to the shortfall below 30. There is no friction in the air.Codeid-Software/QuakeHow we know
207void SV_AirAccelerate (vec3_t wishveloc) ⋮ 212 wishspd = VectorNormalize (wishveloc); 213 if (wishspd > 30) 214 wishspd = 30; 215 currentspeed = DotProduct (velocity, wishveloc); 216 addspeed = wishspd - currentspeed; 217 if (addspeed <= 0) 218 return; 219// accelspeed = sv_accelerate.value * host_frametime; 220 accelspeed = sv_accelerate.value*wishspeed * host_frametime; 221 if (accelspeed > addspeed) 222 accelspeed = addspeed; ⋮
WinQuake/sv_user.c lines 207–225 at bf4ac42 · upstream ↗ 361 else if ( onground ) ⋮ 363 SV_UserFriction (); 364 SV_Accelerate (); ⋮ 366 else 367 { // not on ground, so little effect on velocity 368 SV_AirAccelerate (wishvel);
WinQuake/sv_user.c lines 361–368 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.
- ^ abcDoom applies forward and side thrust at 90° to each other and caps each move separately, so running forward (50) while running sideways (40) gives √(50² + 40²) ≈ 64, 1.28 times the forward run alone.Codeid-Software/DOOM
How we know
160 if (cmd->forwardmove && onground) 161 P_Thrust (player, player->mo->angle, cmd->forwardmove*2048); ⋮ 163 if (cmd->sidemove && onground) 164 P_Thrust (player, player->mo->angle-ANG90, cmd->sidemove*2048);
linuxdoom-1.10/p_user.c lines 160–164 at a77dfb9 · upstream ↗ 413 if (forward > MAXPLMOVE) 414 forward = MAXPLMOVE; ⋮ 417 if (side > MAXPLMOVE) 418 side = MAXPLMOVE;
linuxdoom-1.10/g_game.c lines 413–420 at a77dfb9 · upstream ↗ 171#define MAXPLMOVE (forwardmove[1]) ⋮ 175fixed_t forwardmove[2] = {0x19, 0x32}; 176fixed_t sidemove[2] = {0x18, 0x28};
linuxdoom-1.10/g_game.c lines 171–176 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
- ^ abHolding Doom's strafe modifier makes the turn keys add a side move, and the strafe keys add theirs on top, so a turn key and a strafe key pressed together reach 80, clamped to
MAXPLMOVE(50); a run then moves at √(50² + 50²) ≈ 70.7, 1.41 times the forward run alone.Codeid-Software/DOOMHow we know
277 // let movement keys cancel each other out 278 if (strafe) ⋮ 280 if (gamekeydown[key_right]) ⋮ 283 side += sidemove[speed];
linuxdoom-1.10/g_game.c lines 277–289 at a77dfb9 · upstream ↗ 322 if (gamekeydown[key_straferight]) 323 side += sidemove[speed]; 324 if (gamekeydown[key_strafeleft]) 325 side -= sidemove[speed];
linuxdoom-1.10/g_game.c lines 322–325 at a77dfb9 · upstream ↗ 413 if (forward > MAXPLMOVE) 414 forward = MAXPLMOVE; ⋮ 417 if (side > MAXPLMOVE) 418 side = MAXPLMOVE;
linuxdoom-1.10/g_game.c lines 413–420 at a77dfb9 · upstream ↗ 171#define MAXPLMOVE (forwardmove[1]) ⋮ 176fixed_t sidemove[2] = {0x18, 0x28};
linuxdoom-1.10/g_game.c lines 171–176 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
- ^ abcdDuke Nukem 3D clamps the forward and side moves apart and adds them as perpendicular pushes, so running forward (80) while running sideways (80) gives √2 ≈ 1.41 times the forward run alone (computed); holding the strafe modifier with a turn key and a strafe key together reaches the side move's cap of 90, about 1.51 times. Doom's diagonal run, 50 forward and 40 sideways, is 1.28 times its forward run.Code3D Realms, duke3dsource.zip · id-Software/DOOM
How we know
1925 if (BUTTON(gamefunc_Strafe)) ⋮ 1927 if ( BUTTON(gamefunc_Turn_Left)) ⋮ 1929 svel -= -keymove; ⋮ 1931 if ( BUTTON(gamefunc_Turn_Right)) ⋮ 1933 svel -= keymove;
source/PLAYER.C lines 1925–1934 at e75bb06 · upstream ↗ 1968 if ( BUTTON( gamefunc_Strafe_Left ) ) 1969 svel += keymove; ⋮ 1974 if ( BUTTON(gamefunc_Move_Forward) ) 1975 vel += keymove; ⋮ 1981 if(vel > MAXVEL) vel = MAXVEL; ⋮ 1983 if(svel > MAXSVEL) svel = MAXSVEL;
source/PLAYER.C lines 1965–1983 at e75bb06 · upstream ↗ 2004 momx = mulscale9(vel,sintable[(daang+2560)&2047]); 2005 momy = mulscale9(vel,sintable[(daang+2048)&2047]); ⋮ 2007 momx += mulscale9(svel,sintable[(daang+2048)&2047]); 2008 momy += mulscale9(svel,sintable[(daang+1536)&2047]);
source/PLAYER.C lines 2004–2008 at e75bb06 · upstream ↗ 160 if (cmd->forwardmove && onground) 161 P_Thrust (player, player->mo->angle, cmd->forwardmove*2048); ⋮ 163 if (cmd->sidemove && onground) 164 P_Thrust (player, player->mo->angle-ANG90, cmd->sidemove*2048);
linuxdoom-1.10/p_user.c lines 160–164 at a77dfb9 · upstream ↗ 175fixed_t forwardmove[2] = {0x19, 0x32}; 176fixed_t sidemove[2] = {0x18, 0x28};
linuxdoom-1.10/g_game.c lines 175–176 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcDescent 3's input code clamps the forward, up, and sideways thrust controls to ±1 each, separately, and the released source adds the three pushes along the ship's perpendicular axes, so all three held at once push √3 ≈ 1.73 times as hard as forward alone, and, computed from the linear drag, the ship settles at 1.73 times the speed; Doom does the same with two axes, clamping its forward and side moves apart and thrusting them at 90°.CodeDescentDevelopers/Descent3 · id-Software/DOOM
How we know
896 if (controls->sideways_thrust > LIMIT_HORIZ) controls->sideways_thrust = LIMIT_HORIZ; ⋮ 898 if (controls->vertical_thrust > LIMIT_VERT) controls->vertical_thrust = LIMIT_VERT; ⋮ 901 if (controls->forward_thrust > LIMIT_FORWARD) controls->forward_thrust = LIMIT_FORWARD;
Descent3/Controls.cpp lines 896–902 at 61bb9a3 · upstream ↗ 433#define LIMIT_HORIZ 1.0f 434#define LIMIT_VERT 1.0f 435#define LIMIT_FORWARD 1.0f
Descent3/Controls.cpp lines 433–435 at 61bb9a3 · upstream ↗ 2583 objp->mtype.phys_info.thrust = speed_scalar*Players[objp->id].movement_scalar * ((objp->orient.fvec * controls.forward_thrust * objp->mtype.phys_info.full_thrust) + 2584 (objp->orient.uvec * controls.vertical_thrust * objp->mtype.phys_info.full_thrust) + 2585 (objp->orient.rvec * controls.sideways_thrust * objp->mtype.phys_info.full_thrust));
Descent3/object.cpp lines 2583–2585 at 61bb9a3 · upstream ↗ 160 if (cmd->forwardmove && onground) 161 P_Thrust (player, player->mo->angle, cmd->forwardmove*2048); ⋮ 163 if (cmd->sidemove && onground) 164 P_Thrust (player, player->mo->angle-ANG90, cmd->sidemove*2048);
linuxdoom-1.10/p_user.c lines 160–164 at a77dfb9 · upstream ↗ 413 if (forward > MAXPLMOVE) 414 forward = MAXPLMOVE; ⋮ 417 if (side > MAXPLMOVE) 418 side = MAXPLMOVE;
linuxdoom-1.10/g_game.c lines 413–420 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abComputed from
SV_AirAccelerate, not measured: with a wish speed of 320 the push can reach 10 × 320 × frame time (44.4 at 1/72 s, more at longer frames), so a frame lifts the velocity's component c along the wish direction to 30 whenever c is between −14.4 and 30, and the new speed is √(v² + 900 − c²) for speed v. That is largest with the wish at 90° to the motion: each frame then adds √(v² + 900) − v, 1.40 units/s at 320 and 0.90 at 500, and turns the motion by atan(30/v), 5.4° per frame at 320. At 72 frames a second that is about 101 units/s gained per second at 320.Codeid-Software/QuakeHow we know
212 wishspd = VectorNormalize (wishveloc); 213 if (wishspd > 30) 214 wishspd = 30; 215 currentspeed = DotProduct (velocity, wishveloc); 216 addspeed = wishspd - currentspeed; ⋮ 220 accelspeed = sv_accelerate.value*wishspeed * host_frametime; 221 if (accelspeed > addspeed) 222 accelspeed = addspeed;
WinQuake/sv_user.c lines 212–224 at bf4ac42 · upstream ↗ 168cvar_t sv_accelerate = {"sv_accelerate", "10"};WinQuake/sv_user.c line 168 at bf4ac42 · upstream ↗ 505 if (!cls.timedemo && realtime - oldrealtime < 1.0/72.0)WinQuake/host.c line 505 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.
- ^ abcQuake II's keyboard moves are
cl_forwardspeed,cl_sidespeed, andcl_upspeed200, all doubled while running, which the speed key toggles: withcl_run0 holding the key runs, and withcl_run1 holding it walks. The movement code builds the wish velocity from the view's forward and right vectors, with the pitch first divided by 3, then zeroes its vertical part and caps its length at 300. Computed from these, with the view level (pitch 0): walking forward wishes 200 and walking forward with a strafe about 283, but a run wishes 400 forward and about 566 on the diagonal, both cut to 300, so a strafe adds nothing to a run on the ground. Looking up or down shortens the forward part before the cap.Codeid-Software/Quake-2revisedHow we know
1441 cl_upspeed = Cvar_Get ("cl_upspeed", "200", 0); 1442 cl_forwardspeed = Cvar_Get ("cl_forwardspeed", "200", 0); 1443 cl_sidespeed = Cvar_Get ("cl_sidespeed", "200", 0); ⋮ 1448 cl_run = Cvar_Get ("cl_run", "0", CVAR_ARCHIVE);
client/cl_main.c lines 1441–1448 at 372afde · upstream ↗ 289 cmd->sidemove += cl_sidespeed->value * CL_KeyState (&in_moveright); ⋮ 297 cmd->forwardmove += cl_forwardspeed->value * CL_KeyState (&in_forward); ⋮ 304 if ( (in_speed.state & 1) ^ (int)(cl_run->value) ) ⋮ 306 cmd->forwardmove *= 2; 307 cmd->sidemove *= 2; 308 cmd->upmove *= 2;
client/cl_input.c lines 289–309 at 372afde · upstream ↗ 1344 VectorCopy(pm->viewangles, angles); 1345 if (angles[PITCH] > 180) 1346 angles[PITCH] = angles[PITCH] - 360; 1347 angles[PITCH] /= 3; ⋮ 1349 AngleVectors (angles, pml.forward, pml.right, pml.up);
qcommon/pmove.c lines 1344–1349 at 372afde · upstream ↗ 596 wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; 597 wishvel[2] = 0; ⋮ 602 wishspeed = VectorNormalize(wishdir); ⋮ 607 maxspeed = (pm->s.pm_flags & PMF_DUCKED) ? pm_duckspeed : pm_maxspeed; ⋮ 609 if (wishspeed > maxspeed)
qcommon/pmove.c lines 595–613 at 372afde · 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.
- ^ abIn the air, Quake II's
PM_AirMovecalls the ground'sPM_Acceleratewith an acceleration of 1 whilepm_airaccelerateis 0: the push is 1 × step time × wish speed (up to 300), and it stops only when the velocity's component along the wish direction reaches the full wish speed. Quake III keeps this rule, withpm_airaccelerate1 and aPM_Acceleratecommented "q2 style". In deathmatch the server copies its latched cvarsv_airaccelerate(default 0) intopm_airaccelerate, and any nonzero value switches toPM_AirAccelerate, which is Quake's air rule: the wish speed capped at 30 for the shortfall, the push from the uncapped wish speed ×pm_accelerate(10) × step time. The server's movement code uses the value only as a nonzero test, so it acts as an on-off switch; outside deathmatch it is forced to 0. The server also sends the value to clients as theCS_AIRACCELconfig string, for their prediction, in code not read here.Codeid-Software/Quake-2 · id-Software/Quake-III-Arena · id-Software/QuakerevisedHow we know
57float pm_airaccelerate = 0;qcommon/pmove.c line 57 at 372afde · upstream ↗ 652 else 653 { // not on ground, so little effect on velocity 654 if (pm_airaccelerate) 655 PM_AirAccelerate (wishdir, wishspeed, pm_accelerate); 656 else 657 PM_Accelerate (wishdir, wishspeed, 1);
qcommon/pmove.c lines 652–657 at 372afde · upstream ↗ 414void PM_AirAccelerate (vec3_t wishdir, float wishspeed, float accel) ⋮ 419 if (wishspd > 30) 420 wishspd = 30; 421 currentspeed = DotProduct (pml.velocity, wishdir); 422 addspeed = wishspd - currentspeed; 423 if (addspeed <= 0) 424 return; 425 accelspeed = accel * wishspeed * pml.frametime;
qcommon/pmove.c lines 414–427 at 372afde · upstream ↗ 974 sv_airaccelerate = Cvar_Get("sv_airaccelerate", "0", CVAR_LATCH);server/sv_main.c line 974 at 372afde · upstream ↗ 196 if (Cvar_VariableValue ("deathmatch")) ⋮ 198 sprintf(sv.configstrings[CS_AIRACCEL], "%g", sv_airaccelerate->value); 199 pm_airaccelerate = sv_airaccelerate->value; ⋮ 201 else ⋮ 203 strcpy(sv.configstrings[CS_AIRACCEL], "0"); 204 pm_airaccelerate = 0;
server/sv_init.c lines 196–205 at 372afde · upstream ↗ 40float pm_airaccelerate = 1.0f;code/game/bg_pmove.c line 40 at dbe4ddb · upstream ↗ 240static void PM_Accelerate( vec3_t wishdir, float wishspeed, float accel ) { 241#if 1 242 // q2 style
code/game/bg_pmove.c lines 240–242 at dbe4ddb · upstream ↗ 637 PM_Accelerate (wishdir, wishspeed, pm_airaccelerate);code/game/bg_pmove.c line 637 at dbe4ddb · upstream ↗ 212 wishspd = VectorNormalize (wishveloc); 213 if (wishspd > 30) 214 wishspd = 30; 215 currentspeed = DotProduct (velocity, wishveloc); 216 addspeed = wishspd - currentspeed; ⋮ 220 accelspeed = sv_accelerate.value*wishspeed * host_frametime;
WinQuake/sv_user.c lines 212–220 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.
- ^ abcQuake III's keyboard moves are 127 per axis at a run (the speed key or
cl_run, one but not both) and 64 at a walk, andPM_CmdScalescales the wish so its length is the player's speed × the largest of the forward, side, and up moves ÷ 127, which a comment says avoids "a sqrt(2) distortion in speed": at a run, forward, a strafe, and both together each wish 320, so a strafe adds no ground speed, where Quake's walk with a strafe wishes 320 against 200 (quake/move-speeds). The up move counts too: computed, holding the jump key (up move 127) in the air with forward and a strafe wishes 320 × √2/√3 ≈ 261. On the groundPM_CheckJumpzeroes a held jump's up move, andPmovelowers it to 20 for the later steps of a command it splits. The figures leave out the roundingtrap_SnapVectorapplies to the velocity after each step, engine code the atlas hasn't read.Codeid-Software/Quake-III-Arena · id-Software/QuakerevisedHow we know
317 if ( in_speed.active ^ cl_run->integer ) { 318 movespeed = 127; ⋮ 322 movespeed = 64; ⋮ 333 side += movespeed * CL_KeyState (&in_moveright); 334 side -= movespeed * CL_KeyState (&in_moveleft); ⋮ 337 up += movespeed * CL_KeyState (&in_up); ⋮ 340 forward += movespeed * CL_KeyState (&in_forward); ⋮ 343 cmd->forwardmove = ClampChar( forward ); 344 cmd->rightmove = ClampChar( side ); 345 cmd->upmove = ClampChar( up );
code/client/cl_input.c lines 317–345 at dbe4ddb · upstream ↗ 285Returns the scale factor to apply to cmd movements 286This allows the clients to use axial -127 to 127 values for all directions 287without getting a sqrt(2) distortion in speed. ⋮ 295 max = abs( cmd->forwardmove ); 296 if ( abs( cmd->rightmove ) > max ) { 297 max = abs( cmd->rightmove ); ⋮ 299 if ( abs( cmd->upmove ) > max ) { 300 max = abs( cmd->upmove ); ⋮ 306 total = sqrt( cmd->forwardmove * cmd->forwardmove 307 + cmd->rightmove * cmd->rightmove + cmd->upmove * cmd->upmove ); 308 scale = (float)pm->ps->speed * max / ( 127.0 * total );
code/game/bg_pmove.c lines 285–308 at dbe4ddb · upstream ↗ 627 for ( i = 0 ; i < 2 ; i++ ) { 628 wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; ⋮ 630 wishvel[2] = 0; ⋮ 632 VectorCopy (wishvel, wishdir); 633 wishspeed = VectorNormalize(wishdir); 634 wishspeed *= scale;
code/game/bg_pmove.c lines 627–634 at dbe4ddb · upstream ↗ 369 // must wait for jump to be released 370 if ( pm->ps->pm_flags & PMF_JUMP_HELD ) { 371 // clear upmove so cmdscale doesn't lower running speed 372 pm->cmd.upmove = 0;
code/game/bg_pmove.c lines 369–373 at dbe4ddb · upstream ↗ 2061 if ( pmove->ps->pm_flags & PMF_JUMP_HELD ) { 2062 pmove->cmd.upmove = 20;
code/game/bg_pmove.c lines 2061–2062 at dbe4ddb · upstream ↗ 2015 trap_SnapVector( pm->ps->velocity );code/game/bg_pmove.c line 2015 at dbe4ddb · upstream ↗ 213cvar_t cl_forwardspeed = {"cl_forwardspeed","200", true}; 214cvar_t cl_backspeed = {"cl_backspeed","200", true}; 215cvar_t cl_sidespeed = {"cl_sidespeed","350"};
WinQuake/cl_input.c lines 213–215 at bf4ac42 · upstream ↗ 351 if (wishspeed > sv_maxspeed.value) ⋮ 353 VectorScale (wishvel, sv_maxspeed.value/wishspeed, wishvel); 354 wishspeed = sv_maxspeed.value;
WinQuake/sv_user.c lines 351–354 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.
- ^ abIn the air, Quake III's
PM_AirMoveruns friction (no ground friction, which needs the walking flag), builds a flat wish velocity, and calls the samePM_Accelerateas the ground withpm_airaccelerate1: the push is 1 × step time × wish speed (2.56 units/s per 8 ms step at 320), and it stops only when the velocity's component along the wish direction reaches the full wish speed. Quake's air rule is the reverse: a push ten times as strong (10 × 320 × frame time), cut off when that component reaches 30 (quake/air-accelerate). An#elsebranch left out by#if 1would push the velocity toward the wish velocity instead, commented "proper way (avoids strafe jump maxspeed bug), but feels bad". The figures leave out the roundingtrap_SnapVectorapplies to the velocity after each step, engine code the atlas hasn't read.Codeid-Software/Quake-III-Arena · id-Software/QuakerevisedHow we know
40float pm_airaccelerate = 1.0f;code/game/bg_pmove.c line 40 at dbe4ddb · upstream ↗ 610 PM_Friction(); ⋮ 628 wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; ⋮ 630 wishvel[2] = 0; ⋮ 633 wishspeed = VectorNormalize(wishdir); 634 wishspeed *= scale; ⋮ 636 // not on ground, so little effect on velocity 637 PM_Accelerate (wishdir, wishspeed, pm_airaccelerate);
code/game/bg_pmove.c lines 610–637 at dbe4ddb · upstream ↗ 197 if ( pml.walking && !(pml.groundTrace.surfaceFlags & SURF_SLICK) ) {code/game/bg_pmove.c line 197 at dbe4ddb · upstream ↗ 2015 trap_SnapVector( pm->ps->velocity );code/game/bg_pmove.c line 2015 at dbe4ddb · upstream ↗ 240static void PM_Accelerate( vec3_t wishdir, float wishspeed, float accel ) { 241#if 1 ⋮ 246 currentspeed = DotProduct (pm->ps->velocity, wishdir); 247 addspeed = wishspeed - currentspeed; 248 if (addspeed <= 0) { 249 return; ⋮ 251 accelspeed = accel*pml.frametime*wishspeed; 252 if (accelspeed > addspeed) { 253 accelspeed = addspeed; ⋮ 259#else 260 // proper way (avoids strafe jump maxspeed bug), but feels bad ⋮ 267 VectorSubtract( wishVelocity, pm->ps->velocity, pushDir ); ⋮
code/game/bg_pmove.c lines 240–275 at dbe4ddb · upstream ↗ 212 wishspd = VectorNormalize (wishveloc); 213 if (wishspd > 30) 214 wishspd = 30; 215 currentspeed = DotProduct (velocity, wishveloc); 216 addspeed = wishspd - currentspeed; ⋮ 220 accelspeed = sv_accelerate.value*wishspeed * host_frametime; 221 if (accelspeed > addspeed) 222 accelspeed = addspeed;
WinQuake/sv_user.c lines 212–224 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.
- ^ abComputed from
PM_Acceleratewithpm_airaccelerate1, not measured: at a wish speed W of 320 (forward and a strafe at a run, the jump key up, no Haste) the air push is W × step time, a = 2.56 per 8 ms step, and the cap leaves room for all of it while the velocity's component c along the wish direction is at most W − a; the new speed √(v² + 2ca + a²) is then largest at c = W − a, with the wish 7° off a 320 motion, 37° off 400, and 51° off 500. Each 8 ms step adds 2.54 units/s at 320 and 1.63 at 500, about 317 and 204 units/s each second, close to W²/v at any step length (316 at 1/72 s). Quake's gain is fixed per frame instead, about 101 units/s each second at 320 at 72 frames a second (quake/air-strafe-gain). The figures leave out the roundingtrap_SnapVectorapplies to the velocity after each step, engine code the atlas hasn't read.Codeid-Software/Quake-III-Arena · id-Software/QuakerevisedHow we know
246 currentspeed = DotProduct (pm->ps->velocity, wishdir); 247 addspeed = wishspeed - currentspeed; 248 if (addspeed <= 0) { 249 return; ⋮ 251 accelspeed = accel*pml.frametime*wishspeed; 252 if (accelspeed > addspeed) { 253 accelspeed = addspeed;
code/game/bg_pmove.c lines 246–254 at dbe4ddb · upstream ↗ 40float pm_airaccelerate = 1.0f;code/game/bg_pmove.c line 40 at dbe4ddb · upstream ↗ 637 PM_Accelerate (wishdir, wishspeed, pm_airaccelerate);code/game/bg_pmove.c line 637 at dbe4ddb · upstream ↗ 1909 pml.frametime = pml.msec * 0.001;code/game/bg_pmove.c line 1909 at dbe4ddb · upstream ↗ 2015 trap_SnapVector( pm->ps->velocity );code/game/bg_pmove.c line 2015 at dbe4ddb · upstream ↗ 179 { &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse},code/game/g_main.c line 179 at dbe4ddb · upstream ↗ 212 wishspd = VectorNormalize (wishveloc); 213 if (wishspd > 30) 214 wishspd = 30; 215 currentspeed = DotProduct (velocity, wishveloc); 216 addspeed = wishspd - currentspeed; ⋮ 220 accelspeed = sv_accelerate.value*wishspeed * host_frametime;
WinQuake/sv_user.c lines 212–222 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.
- ^QuakeWorld's
PM_AirAcceleratekeeps WinQuake's air rule unchanged: the wish speed capped at 30 for the shortfall test, and the push from the uncapped wish speed × acceleration × frame time; it adds only an early return for a dead player or a water jump.Codeid-Software/QuakeHow we know
412void PM_AirAccelerate (vec3_t wishdir, float wishspeed, float accel) ⋮ 417 if (pmove.dead) 418 return; 419 if (pmove.waterjumptime) 420 return; ⋮ 422 if (wishspd > 30) 423 wishspd = 30; 424 currentspeed = DotProduct (pmove.velocity, wishdir); 425 addspeed = wishspd - currentspeed; ⋮ 427 return; 428 accelspeed = accel * wishspeed * frametime; 429 if (accelspeed > addspeed) ⋮
QW/client/pmove.c lines 412–433 at bf4ac42 · upstream ↗ 212 wishspd = VectorNormalize (wishveloc); 213 if (wishspd > 30) 214 wishspd = 30; 215 currentspeed = DotProduct (velocity, wishveloc); 216 addspeed = wishspd - currentspeed; ⋮ 220 accelspeed = sv_accelerate.value*wishspeed * host_frametime; 221 if (accelspeed > addspeed) 222 accelspeed = addspeed;
WinQuake/sv_user.c lines 212–222 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.
Sources
Code
- Software, id. 2012. “Wolfenstein 3D: the source release (id-Software/wolf3d).” Commit 0516778. id-Software/wolf3d @ 0516778 · licence, all uses
- 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. 2024. “DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10).” Commit a77dfb9. id-Software/DOOM @ a77dfb9 · licence, all uses
- Realms, 3D. 2003. “Duke Nukem 3D: 3D Realms' source release (version 1.5, 2003).” Commit e75bb06. commercial-game-sources/duke_nukem_3d @ e75bb06 · licence, all uses
- Entertainment, Outrage. 2024. “Descent 3: the source release (DescentDevelopers/Descent3, initial import).” Commit 61bb9a3. DescentDevelopers/Descent3 @ 61bb9a3 · licence, all uses
- Software, id. 2012. “Quake II: the GPL source release (id-Software/Quake-2, 3.19).” Commit 372afde. id-Software/Quake-2 @ 372afde · 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