The Mathematics of B-Hopping: You don't have to decelerate if you call Jump() before ApplyFriction()



Once an accidental product of limiting the acceleration of a kinematic character controller, now an intentional feature in many classic-style arena shooters, b-hopping ("bunnyhopping", "airstrafing") is a combination of programmer errors that allows players to extend beyond the intended walking and jumping designed by the developers.
B-Hopping in most contexts (in particular Source Engine games e.g. the Team Fortress series, the Counter Strike series) refers to a combination of two mechanics: bunny-hopping and air-strafing. The former refers to jumping in a frame-perfect manner upon reaching the ground, such that the player is able to jump again but never experiences friction. This is due to the order in which these effects are applied in the update function; below is pseudocode illustrating this.


       

            function Update(float timeStep) {
                boolean grounded = CheckGrounded(); //Checks to see if the player is grounded
                ...
                if(grounded) {
                    Accelerate(inputDirection, groundAcceleration); //Move the player in their input direction (based on WASD input + player rotation), multiplied by the walking acceleration
                } else {
                    Accelerate(inputDirection, airAcceleration); //"Glide" the player; air acceleration is typically very small and only barely accelerates the player
                }
                ...
                if(inputJump and grounded) {
                    jump();
                    grounded = false; //We've called the jump function that pushes us upward - it's important that we set grounded to false to avoid the If block below being called
                }
                ...
                if(grounded) {
                    ApplyFriction(); //Kinematic bodies don't really have mass/normal reaction force, so the maximum friction at any point is probably going to be an acceleration value
                }
            }

       
 

The above code, explicitly in the order shown above, allows a player to avoid ground-friction, which with kinematic characters is managed "manually" (i.e. in the update cycle, without considering mass), and continue to move at their in-air speed between jumps.
The above code works as such due to the following reasons:

  1. The decision between using on-ground acceleration and in-air acceleration is done before the jump logic.
  2. Jumping is done before friction is applied.
  3. Jumping forces grounded to be false.
This ensures that the player can benefit from the much larger ground-acceleration value, giving them much more speed than the air acceleration value would, while un-grounding the player before applying friction, such that they accelerate at "grounded" values but decelerate at "in-air" values (i.e. not at all, assuming there's no air resistance programmed).


The second half of B-Hopping is commonly known as air-strafing. Originally, this was an oversight by developers that were attempting to limit player-input acceleration (i.e. you can't walk beyond a speed limit) without preventing external forces from accelerating the player up to insane speeds e.g. allowing an explosion flinging the player at great speed through the air, but not allowing the player to reach the same speeds just using WASD-inputs).
The Accelerate method above is detailed in pseudocode below:
       

            function Accelerate(Vector3 accelerationDirection, float acceleration) {
                Vector3 velocity = GetVelocity() // Don't normalise
                float projectedVelocity = velocity.dot(accelerationDirection); // How similar are our velocity direction and our "desired" acceleration direction?
                float accelerationThisFrame = acceleration * deltaTime; // Multiply by the amount of time passing this frame to get the per-second value
                
                if(projectedVelocity + accelerationThisFrame > maxWalkSpeed) { // Truncate the acceleration so that the projection doesn't exceed our maximum speed
                    accelerationThisFrame = maxWalkSpeed - projVel;
                }
                
                SetVelocity(velocity + accelerationDirection * accelerationThisFrame)
            }

       
 

This means that if we're attempting to walk/glide toward our current velocity, the result is that we don't accelerate at all - in fact, we slow down (the projection will be larger than the maximum speed, therefore "maxSpeed - projection" will be negative.
Conversely, if we accelerate at 90° to our current velocity, we increase the length of our velocity vector. This allows us to have a net-acceleration per frame by pointing the player in the direction of travel (i.e. aim at our velocity), and use the A and D keys to perpendicularly accelerate without being hindered by the speed limit.


This combination of bunny-hopping and air-strafing is what allows players to build speed without ever experiencing friction or technically breaking the speed limit. It has since been fixed in several titles (notably Half Life 2, and almost every non-Quake-style FPS game), but remains intentionally included in titles hoping to revive the fast-paced shooter genre such as DUSK.

Comments

Popular Posts