What exactly is "air-strafing"?

  • Air strafing is a technique in which a player would sync strafe keys and mouse movements when in-air. (For example, if you press the right strafe key, you would move your mouse rightwards smoothly as you hold the key).
  • It had spawned an exploit - a player can exceed maximum velocity; this will give the player additional speed.
  • It was unintentionally created (as a bug) back in Quake.


So why exactly does air-strafing work? What is the specific anomaly/cause?

  • Instead of limiting velocity directly, only the projection of the current velocity onto acceleration is limited.
  • This way, in certain scenarios, the velocity projection is less than the actual velocity!
  • $\begingroup$ About mass - you can multiply the accelerate by player.mass. $\endgroup$ Feb 18 '16 at 19:33
  • $\begingroup$ @Adrians Netlis I am certain that applyForce() already incorporates mass with the force it adds. The problem arrives when the to-be-added velocity is truncated. This fixes problems with velocity (to avoid passing the cap); however, the player will decelerate when he is reaching his maximum velocity. He will accelerate at what seems to be a fixed rate, but will start to accelerate more slowly as he gets near the cap. This creates for an unwanted result: the player will never reach his maximum velocity. He may, but I have not seen it happen; the player will get very near. $\endgroup$
    – blackhole
    Feb 18 '16 at 22:14
  • $\begingroup$ You can remove translation damping. However, this behaviour is more than realistic. It is what's known as drag force. You should be happy that it accidently appeared there:D $\endgroup$ Feb 19 '16 at 6:12
  • $\begingroup$ Ahah! I found it! The entire time I was using player.getLinearVelocity(), not player.getLinearVelocity(True). I assumed "True" was default. Although, I've spent tons of hours and have about ~25 .blend files with different approaches. Just about all of them worked when I fixed the velocity error. :< $\endgroup$
    – blackhole
    Mar 3 '16 at 0:34
  • $\begingroup$ I'll be updating the question and posting an answer and a .blend. ;) $\endgroup$
    – blackhole
    Mar 3 '16 at 1:09

it's simple really,

just use seperate logic to apply forces on axis independently, and have each key only apply a cap to it's own directions

if keypressW.positive:
    if own.localLinearVelocity.x<10:
if keypressE.positive:
    if own.localLinearVelocity.y<10:

this way X(10) + Y(10) is greater then X(10) could ever be.

  • 1
    $\begingroup$ From observing the code, it just seems that there will be an unbalance of velocity when using this code: the player's diagonal movement (AW, AS, WD, SD) will be faster than usual. This is not strafing. $\endgroup$
    – blackhole
    Feb 18 '16 at 22:17
  • $\begingroup$ I am sorry, this is what I gathered you wanted from my initial read through, I have read it over again, and it is indeed complex, I will try and take a crack at it. $\endgroup$ Feb 23 '16 at 7:15
  • $\begingroup$ I just happened to find that all of my recent approaches seem correct. It just won't work, for adding force doesn't directly add to x, y, and z, but adds the given force over a period of time; this time seems to be determined by the objects mass. I have been trying different ways, but all I need is a way to directly add a value to the velocity, not adding a force. $\endgroup$
    – blackhole
    Feb 23 '16 at 21:49
  • $\begingroup$ Try using if localLinearVelocity.x<= (max - incriment): localLinearVelocity.x += incriment $\endgroup$ Feb 23 '16 at 22:37
  • 4
    $\begingroup$ Ah. I can't believe I put up 150 reputation for this. The entire time I had the correct code, I just wasn't using what you've suggested. Other than one small bug that I think is an easy fix, everything works as expected. Thanks. I will be putting up an answer soon if everything goes well. $\endgroup$
    – blackhole
    Feb 23 '16 at 22:55


  • Refer to this page for an explanation of bhopping as a whole (air-strafing is a part of bhopping).
  • It seems that the original developers tried to do it the original way, but they've ran into an issue of some sort. They've then tried a different attempt, and this is where the exploit started.


As mentioned, the following is the reason why this is all possible: instead of limiting velocity directly, only the projection of the current velocity onto acceleration is limited. Usually, you would have a value for max velocity and a vector for velocity. When the player intends to move, the difference from max velocity from the length of the velocity vector will be added (to the velocity). In this case, the vector projection of the current velocity and the normalized acceleration direction is used as the velocity length (, not the actual velocity length). The acceleration is a server-defined property that determines how much to add to the current velocity. Since the acceleration direction vector is normalized, this all works out: the acceleration is multiplied to the acceleration direction, and this value will be added to the current velocity vector. This is if the acceleration value is not truncated; the acceleration value is truncated if the current velocity projection plus the acceleration value exceeds the max velocity. It will be truncated to the max velocity minus the velocity projection.

Vector Projection

The vector projection of a on b is a vector whose magnitude is the scalar projection of a on b and whose angle against b is either 0 or 180 degrees.

Scalar Projection:


Vector Projection:


Unit Vector:


Alternatively, we can simply use a dot product.

Implementation in Code

import bge
from bge import logic
import math
from mathutils import Vector
import numpy

cont = logic.getCurrentController()
player = cont.owner
keyboard = bge.logic.keyboard
input_active = bge.logic.KX_INPUT_ACTIVE

#acceleration keysx
forward_input_key = "WKEY"    # Forward
backward_input_key = "SKEY"   # Backward
leftward_input_key = "AKEY"   # Leftward
rightward_input_key = "DKEY"  # Rightward

#acceleration inputs
forward_input = keyboard.events[getattr(bge.events, forward_input_key)]
backward_input = keyboard.events[getattr(bge.events, backward_input_key)]
leftward_input = keyboard.events[getattr(bge.events, leftward_input_key)]
rightward_input = keyboard.events[getattr(bge.events, rightward_input_key)]

#server-defined properties
svr_accelerate_air = 1.2     # These properties are server-defined. Increasing svr_accelerate_air will make
svr_velocity_max_air = 10     # air-strafing easier, and svr_velocity_max_air is the max in-air velocity

velocity = player.getLinearVelocity(True)
player["velocity"] = velocity.length

accelerate_direction = Vector()

if forward_input == input_active:                    # Every tick, this will determine where the player is intending to go.
  accelerate_direction += Vector((0.0, 1.0, 0.0))    # Pressing any movement key will change this value.

if backward_input == input_active:
  accelerate_direction += Vector((0.0, -1.0, 0.0))

if leftward_input == input_active:
  accelerate_direction += Vector((-1.0, 0.0, 0.0))

if rightward_input == input_active:
  accelerate_direction += Vector((1.0, 0.0, 0.0))

accelerate_direction.normalize()    # This is crucial to the result!

def playerAccelerate(velocity, velocity_max, accelerate_direction, accelerate):

    velocity_projection = numpy.dot(velocity, accelerate_direction)
    velocity_accelerate = accelerate

    if velocity_projection + velocity_accelerate > velocity_max:    # Here, we truncate the acceleration value
        velocity_accelerate = velocity_max - velocity_projection    # if it will make for an unbalanced total velocity.

    player.localLinearVelocity += accelerate_direction * velocity_accelerate    # Acceleration multiplied by accelerate_direction will be added
                                                                                # to the current velocity. This is why normalizing the vector
                                                                                # is crucial!

#apply acceleration
playerAccelerate(velocity, svr_velocity_max_air, accelerate_direction, svr_accelerate_air)    # Execute playerAccelerate() every tick. This is
                                                                                              # done in this format to make adding ground acceleration
                                                                                              # easier to implement.


  • The variables use "air" in the name as, in order to create a full movement system, ground acceleration will also be needed. Ground acceleration will have its own set of values.

Final Result

I have compiled all the work in a .blend file very neatly. Hopefully, there is nothing misleading or confusing. Everything works perfectly (that I know of). The final result:


  • The file contains explanation and instructions for air-strafing.
  • Here is an example of the exploit: you can see that my velocity is over the server-defined max, but I am using air-strafing to exploit it!
  • $\begingroup$ Oh! And make sure to add velocity[2] = 0 after velocity is defined in order to make sure it's only the x and y axis, not z. $\endgroup$
    – blackhole
    Mar 3 '16 at 2:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.