⇠ Luna’s Blog

Gordon Freeman at the Olympic Games

Since the dawn of October 1st 2022, the world has been plagued by the question:

Would accelerated backhopping give you a competitive advantage in the 100m dash?

In this deranged ramble blog article we shall attempt to answer the question once and for all.


“Gordon! You must get out of here! Run!” — Dr. Kleiner, Half-Life 2

Initial Parameters

Our investigation into the viability of accelerated backhopping as a competitive strategy in the 100m dash will use the following parameters:


What is Accelerated Backhopping?

Note: the technical aspects of this section are largely adapted from the SourceRuns wiki.

Accelerated backhopping (henceforth referred to as ABH) is a bug in the Source engine movement code, allowing the player to move far faster than the developers intended.

When the player performs a jump, the game tries to give the player a little extra speed boost, based on their current velocity, state (crouching, sprinting, etc), movement input, and look direction. It also attempts to cap the player’s speed, but an oversight in the programming causes the player to accelerate far beyond that speed limit under certain circumstances.


Let’s look at the code; this is executed when a player is on the ground and makes a jump input:

Calculate the boost multiplier flSpeedBoostPerc to determine how much of a boost we want to give. If the player is sprinting or ducking, they get a 10% boost, otherwise they get a 50% boost.

float flSpeedBoostPerc = ( !pMoveData->m_bIsSprinting && !player->m_Local.m_bDucked ) ? 0.5f : 0.1f;

Calculate flSpeedAddition, the additional velocity we want to apply to our player’s speed, by scaling the player’s forward movement input mv->m_flForwardMove by our boost multiplier, and taking the absolute value so it’s positive.

float flSpeedAddition = fabs( mv->m_flForwardMove * flSpeedBoostPerc );

Calculate flMaxSpeed, the maximum speed the player should be travelling at. We take mv->m_flMaxSpeed, the maximum speed expected for the player’s current state, and increase it by our boost multiplier.

float flMaxSpeed = mv->m_flMaxSpeed + ( mv->m_flMaxSpeed * flSpeedBoostPerc );

Calculate flNewSpeed, the speed we expect the player to have after jumping, by adding flSpeedAddition to the player’s current lateral velocity mv->m_vecVelocity.Length2D.

float flNewSpeed = ( flSpeedAddition + mv->m_vecVelocity.Length2D() );

If the player’s new speed is higher than the maximum speed, subtract the difference from flSpeedAddition.

if ( flNewSpeed > flMaxSpeed )
{
    flSpeedAddition -= flNewSpeed - flMaxSpeed;
}

If the player is providing a backwards movement input, negate flSpeedAddition:

if ( mv->m_flForwardMove < 0.0f )
    flSpeedAddition *= -1.0f;

Apply the speed addition to the player’s velocity, by adding it in the forward look direction vecForward:

VectorAdd( (vecForward*flSpeedAddition), mv->m_vecVelocity, mv->m_vecVelocity );

The critical flaw in the code resides in the assumption that the movement input and the current velocity are correlated.

Consider the following situation: The player is sprinting along at 320ups (units per second). They jump forwards, and the boost increases their speed to 352ups, by design. While in the air, they spin around 180 degrees (so they’re now facing where they came from, and moving backwards), release all movement inputs, and begin crouching. Upon touching the ground, still moving at 352ups, they immediately jump again.

The player is crouching, so the boost multiplier is set to 10%.

Since the player isn’t providing any forward movement input, flSpeedAddition becomes 0:

float flSpeedAddition = fabs( mv->m_flForwardMove * flSpeedBoostPerc ) 
                      = fabs(0 * 0.1) 
                      = 0

The max speed flMaxSpeed is calculated based on crouching speed limit of 190ups, and comes out as 209:

float flMaxSpeed = mv->m_flMaxSpeed + ( mv->m_flMaxSpeed * flSpeedBoostPerc ) 
                 = 190 + (190 * 0.1) 
                 = 209

Since flSpeedAddition is 0, the new speed flNewSpeed does not change from the player’s current velocity.

float flNewSpeed = ( flSpeedAddition + mv->m_vecVelocity.Length2D() ) 
                 = 0 + 352 
                 = 352

flNewSpeed is now far in excess of flMaxSpeed, so the difference is subtracted from flSpeedAddition, giving -143:

flSpeedAddition -= flNewSpeed - flMaxSpeed;
                = 0 - (352 - 209)
                = -143

The player is not providing any backwards movement input, so flSpeedAddition remains negative.

if ( mv->m_flForwardMove < 0.0f ) // false
    flSpeedAddition *= -1.0f; // doesn't execute

This negative value then gets multiplied by the player’s forward look direction, producing a vector pointing 143 units behind them. This gets applied to the player’s velocity vector — remember, since spinning in mid-air they’re already moving backwards — increasing their speed by 143ups for a new speed of 495ups.

On the next jump, this effect is compounded, creating a speed boost of 286ups, then 572ups, then 1144ups, and so on.


Rules of Engagement

We will now consult the IAAF Book of Rules to determine the conditions under which we will compete. The following excerpts are all taken from IAAF Book C, C2.1 Technical Rules.

Attire

Assistance not Allowed

6.3 For the purpose of this Rule, the following examples shall be considered assistance, and are therefore not allowed:
[...]
6.3.4 The use of any mechanical aid, except by an athlete with an impairment as authorised or permitted in accordance with the Mechanical Aids Regulations.

Wearing the HEV suit allows the player to sprint, allowing for a much faster initial ABH speed than would otherwise be possible (495ups with the suit, 285ups without). We believe it is a reasonable interpretation that the HEV suit constitutes a mechanical aid, and would therefore not be permitted for use. Our runner will thus compete without the suit.

Race Start

16.3 In races up to and including 400m (including the first leg of 4 × 200m, the Medley Relay and 4 × 400m), a crouch start and the use of starting blocks are compulsory. After the “On your marks” command, an athlete shall approach the start line, assume a position completely within their allocated lane and behind the start line. An athlete shall not touch either the start line or the ground in front of it with their hands or their feet when on their mark. Both hands and at least one knee shall be in contact with the ground and both feet in contact with the foot plates of the starting blocks. At the “Set” command, an athlete shall immediately rise to their final starting position retaining the contact of the hands with the ground and of the feet with the foot plates of the blocks. Once the Starter is satisfied that all athletes are steady in the “Set” position, the gun shall be fired.

Within the bounds of Half-Life 2’s movement, the use of starting blocks and placing hands on the ground are not possible, so we will assume reasonable exemption from these rules. However, we can still abide by the requirement of a crouching start.

Lanes

14.4 In all races up to and including 400m, each athlete shall have a separate lane, with a width of 1.22m ± 0.01m, including the lane line on the right, marked by white lines 50mm in width. All lanes shall be of the same nominal width. The inner lane shall be measured as stated in Rule 14.2, but the remaining lanes shall be measured 0.20m from the outer edges of the lines.

17.3 In all races:

17.3.1 run in lanes, each athlete shall keep within their allocated lane from start to finish. This shall also apply to any portion of a race run in lanes;

17.4 An athlete, or in the case of a relay race, their team, shall not be disqualified if the athlete:
[...]
17.4.2 steps or runs outside their lane in the straight, any straight part of the diversion from the track for the steeplechase water jump or outside the outer line of their lane on the bend;
[...]
and no material advantage is gained and no other athlete being jostled or obstructed so as to impede the other athlete’s progress (see Rule 17.2 of the Technical Rules). If material advantage is gained, the athlete (or team) shall be disqualified.

In summary, the lane is 1.22m wide, and we must be careful to stay within that lane. We are only permitted to leave the lane if no material advantage would be gained by doing so; as such we cannot use any maneuvers that depend on us leaving the lane.

The Finish

18.2 The athletes shall be placed in the order in which any part of their bodies (i.e. torso, as distinguished from the head, neck, arms, legs, hands or feet) reaches the vertical plane of the nearer edge of the finish line as defined above.

In summary, our aim is to:


A Matter of Scale

Contemporary game engines typically base their unit systems on the metric system.[1][2] Source engine, however, is an unusual beast, and its units are based on the imperial system. Maps are modelled at a scale of 16 units = 1 foot, or 1 unit = 3/4 inch.[3] From this, we can calculate that our 100m run is 5,249.34 units.


The Run

Note: The following timings and speeds are theoretical based on a crude analysis of the game code, and may not necessarily be reproducible under practical testing.

The player begins from a crouching position at the start line. Since an ABH cannot be performed from a crouching start, they spend 200ms returning to a standing position.[4] Once standing, they begin walking forward, quickly reaching their maximum walking speed of 150ups. Once at speed, they make their first jump facing forwards. They get the intended jump-speed bonus, accelerating them to 225ups. While in the air, they release all forward input, turn 180 degrees, and crouch. Roughly 510ms after jumping,[5] they make their first landing. Still crouching and providing no lateral movement input, they jump again, ABH accelerating them from 225ups to a new speed of 285ups. A further 510ms later, they hit the ground again, and with another jump, ABH accelerates them to 405ups. This continues as follows:

Time* Distance* Speed**
0.00s 0.00u 225ups
0.51s 114.75u 285ups
1.02s 260.10u 405ups
1.53s 466.65u 645ups
2.04s 795.60u 1125ups
2.55s 1369.35u 2085ups
3.06s 2432.70u 3500ups
3.57s 4217.70u 3500ups
4.08s 6002.70u 3500ups

* from initial jump
** during jump

On the seventh jump, ABH would give a velocity of 4005ups, but this gets hard-capped to 3500ups, and remains as such for the remainder of the run. 3.865s after the first jump, the player flies across the finish line, and shortly afterwards at 4.08s, they land some 753.36u (14.35m) beyond it.

Note that these timings and distances are relative to the first jump. After the starting pistol is fired, the player takes 200ms to stand from crouching, and an unspecified amount of time and distance (dependent on the ground friction) to reach their 150ups walking speed. However, given the acceleration the player is able to achieve once jumping, this pre-jump phase would have to last at least 5.715s in order for 100m record-holder Usain Bolt to still have a chance of outrunning them.

We hereby propose that the IAAF add a rule explicitly banning the use of ABH in competitions, and submit this article as justification.


Addendum

Lyren from the SourceRuns team provided two practical runs, one with human inputs [video], and one with host_timescale to slow down time, along with an autojumping script [video].

It could be argued that since the player cannot sprint without the HEV suit, this would constitute an impairment and the suit may be permitted under the Mechanical Aids Regulations. However, as shown, even without the suit, the player clearly has an unfair advantage over conventional human runners; the addition of the suit would just add further insult to injury.

The observant among you will notice that the blog’s accent colors have been changed specifically for this article, to a shade of orange taken from the Orange Box website circa 2007.


  1. Unity uses 1 unit = 1 meter (source

  2. Unreal Engine uses 1 unit = 1 centimeter (source

  3. Curiously, while maps are modeled at 1 unit = 3/4 inch, characters are modeled at a different scale of 1 unit = 1 inch. (source). 

  4. TIME_TO_UNDUCK_MS is defined as 200.0f (shareddefs.h

  5. GAMEMOVEMENT_JUMP_TIME is defined as 510.0f (gamemovement.h