On this page
  1. Separately capped movement
  2. Ground caps and air acceleration
  3. Related
  4. Games
  5. References
  6. Sources

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

GameYearHowNote
Wolfenstein 3D1992Separately capped axeswith 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
Doom1993Separately capped axesforward 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 3D1996Separately capped axesforward and side moves are capped apart at 90, so a diagonal keyboard run is 1.41 times as fast
Descent 31999Separately capped axesforward, up, and sideways are capped apart and summed, so all three at once push 1.73 times as hard as forward alone
Quake1996Air strafeon 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 II1997Angled air strafeon 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 Arena1999Angled air strafeon 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)
Wolfenstein 3D 1992With the strafe button held, Wolfenstein 3D's side move is a second Thrust at 90° to the facing, and PollControls caps 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).Code
Doom 1993Doom 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.Code
Holding 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.Code
Duke Nukem 3D 1996Duke 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.Code
Quake 1996Quake's keyboard moves are cl_forwardspeed and cl_backspeed 200 and cl_sidespeed 350, doubled by the run key (cl_movespeedkey 2.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.Code
In the air, Quake's SV_AirAccelerate takes 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 is sv_accelerate × the uncapped wish speed (up to 320) × frame time, cut to the shortfall below 30. There is no friction in the air.Code
Computed 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.Code
QuakeWorld's PM_AirAccelerate keeps 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.Code
Quake II 1997Quake II's keyboard moves are cl_forwardspeed, cl_sidespeed, and cl_upspeed 200, all doubled while running, which the speed key toggles: with cl_run 0 holding the key runs, and with cl_run 1 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.Code
In the air, Quake II's PM_AirMove calls the ground's PM_Accelerate with an acceleration of 1 while pm_airaccelerate is 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, with pm_airaccelerate 1 and a PM_Accelerate commented "q2 style". In deathmatch the server copies its latched cvar sv_airaccelerate (default 0) into pm_airaccelerate, and any nonzero value switches to PM_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 the CS_AIRACCEL config string, for their prediction, in code not read here.Code
Descent 3 1999Descent 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°.Code
Quake III Arena 1999Quake 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, and PM_CmdScale scales 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 ground PM_CheckJump zeroes a held jump's up move, and Pmove lowers it to 20 for the later steps of a command it splits. The figures leave out the rounding trap_SnapVector applies to the velocity after each step, engine code the atlas hasn't read.Code
In the air, Quake III's PM_AirMove runs friction (no ground friction, which needs the walking flag), builds a flat wish velocity, and calls the same PM_Accelerate as the ground with pm_airaccelerate 1: 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 #else branch left out by #if 1 would push the velocity toward the wish velocity instead, commented "proper way (avoids strafe jump maxspeed bug), but feels bad". The figures leave out the rounding trap_SnapVector applies to the velocity after each step, engine code the atlas hasn't read.Code
Computed from PM_Accelerate with pm_airaccelerate 1, 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 rounding trap_SnapVector applies to the velocity after each step, engine code the atlas hasn't read.Code

References

  1. ^ abcdWith the strafe button held, Wolfenstein 3D's side move is a second Thrust at 90° to the facing, and PollControls caps 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/wolf3d
    How 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.

    In the claim ledger

  2. ^ abcdeQuake's keyboard moves are cl_forwardspeed and cl_backspeed 200 and cl_sidespeed 350, doubled by the run key (cl_movespeedkey 2.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/Quake
    How 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.

    In the claim ledger

  3. ^ abcdeIn the air, Quake's SV_AirAccelerate takes 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 is sv_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/Quake
    How 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.

    In the claim ledger

  4. ^ 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.

    In the claim ledger

  5. ^ 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/DOOM
    How 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.

    In the claim ledger

  6. ^ 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.

    In the claim ledger

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

    In the claim ledger

  8. ^ 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/Quake
    How 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.

    In the claim ledger

  9. ^ abcQuake II's keyboard moves are cl_forwardspeed, cl_sidespeed, and cl_upspeed 200, all doubled while running, which the speed key toggles: with cl_run 0 holding the key runs, and with cl_run 1 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-2revised
    How 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.

    In the claim ledger

  10. ^ abIn the air, Quake II's PM_AirMove calls the ground's PM_Accelerate with an acceleration of 1 while pm_airaccelerate is 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, with pm_airaccelerate 1 and a PM_Accelerate commented "q2 style". In deathmatch the server copies its latched cvar sv_airaccelerate (default 0) into pm_airaccelerate, and any nonzero value switches to PM_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 the CS_AIRACCEL config string, for their prediction, in code not read here.Codeid-Software/Quake-2 · id-Software/Quake-III-Arena · id-Software/Quakerevised
    How 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.

    In the claim ledger

  11. ^ 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, and PM_CmdScale scales 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 ground PM_CheckJump zeroes a held jump's up move, and Pmove lowers it to 20 for the later steps of a command it splits. The figures leave out the rounding trap_SnapVector applies to the velocity after each step, engine code the atlas hasn't read.Codeid-Software/Quake-III-Arena · id-Software/Quakerevised
    How 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.

    In the claim ledger

  12. ^ abIn the air, Quake III's PM_AirMove runs friction (no ground friction, which needs the walking flag), builds a flat wish velocity, and calls the same PM_Accelerate as the ground with pm_airaccelerate 1: 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 #else branch left out by #if 1 would push the velocity toward the wish velocity instead, commented "proper way (avoids strafe jump maxspeed bug), but feels bad". The figures leave out the rounding trap_SnapVector applies to the velocity after each step, engine code the atlas hasn't read.Codeid-Software/Quake-III-Arena · id-Software/Quakerevised
    How 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.

    In the claim ledger

  13. ^ abComputed from PM_Accelerate with pm_airaccelerate 1, 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 rounding trap_SnapVector applies to the velocity after each step, engine code the atlas hasn't read.Codeid-Software/Quake-III-Arena · id-Software/Quakerevised
    How 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.

    In the claim ledger

  14. ^QuakeWorld's PM_AirAccelerate keeps 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/Quake
    How 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.

    In the claim ledger

Sources

Code