Last server records
Pro Nub

Bhop Physics

Posted by Kpoluk 13 May 2023 in 11:58
This time we're going to face fuser2 parameter that was introduced in CS 1.6, so we will have to use pm_shared.cpp from ReGameDLL project instead of pm_shared.cpp from HLSDK.

Let's consider PM_PlayerMove function from the article about strafe physics in more detail:


As you can see, there are several types of movement, and the functions we have considered, such as PM_Friction, PM_WalkMove and PM_AirMove, are called for type MOVETYPE_WALK. With PM_Duck, which is responsible for the duck process, we will get acquainted in the next article, but for now we will be interested in the call of PM_Jump before PM_Friction. Note that this call occurs if IN_JUMP bit is set in the cmd.buttons variable that stores the states of the buttons. When we press the jump button, +jump command is sent to the engine, and IN_JUMP bit becomes 1. When the button is released, the –jump command is sent, and at the same frame IN_JUMP is reset. Read more about this and why we can jump using the scroll, under this spoiler

Let's take a look at input.cpp and find the function InitInput. Here we find that command +jump initiates a call of the function IN_JumpDown, which changes the variable state of the jump button: the first and second bits of state are set. The first bit means that the button is pressed in general, and the second - that the button was pressed just now. The –jump command initiates a call of IN_JumpUp, which clears the first bit (button is no longer pressed) and sets the third bit (the button is released just now).

Further in CL_ButtonBits we find that the IN_JUMP bit in cmd.buttons is set if state has the first or the second bit set. Thus, if we jumped with the help of Space for example, then IN_JUMP will be set starting from the frame when we pressed the Space. At the frame when the Space was released, the IN_JUMP bit has been cleared. If we jumped with the help of a scroll, then the commands +jump and –jump were sent one after another at the same frame. Then at this frame the first bit of state is zero, that is, the jump button is considered not pressed. However, since the second bit of state is set, then IN_JUMP in cmd.buttons will also be set, which means PM_Jump will be called. That's why we can jump with the scroll.


Now it's time to see PM_Jump:


Note that if there was a bit IN_JUMP at the previous frame, then there won't be a jump at the current frame. Otherwise, we could do bhop just by holding the Space. Moreover, this condition also means that the command +jump;-jump sent with the scroll will push us off from the ground only if there was no jump command at the previous frame. How does this affect our bhop?

We introduce the term FOG (frames on the ground) - the number of frames that player spends on the ground during one bhop.

Theoretically bhop can be done with Space, but human reaction is not good enough to send a jump command in time. By scrolling we send several jump commands at once, and it allows us to minimize FOG and lose less speed. However, too fast scrolling will lead to the fact that several jump commands can go in a row, which means that only the first one can cause a jump, the rest will be useless. A good distribution of jump commands is when we alternate frames with +jump and without it. In this case, we guarantee 1 or 2 FOG. In practice, the distribution for every player is different, but, nevertheless, with time everyone succeeds in minimizing the number of bhops with more than 2 FOG.

Horizontal speed


However does lesser FOG always means lesser loss of speed? First of all, it should be clarified that we are interested in horizontal speed now. After all, horizontal speed before hitting the ground is what lj stats shows us as a bhop prestrafe. For simplicity we will not pay attention to the variable fuser2 for now, and also let the vertical speed on the ground be zero. Consider two cases:

1) 1 FOG bhop. A successful jump occurred at the same frame when we were on the ground. Inside PM_Jump, the variable pmove->onground became equal to -1, therefore PM_Friction in PM_PlayerMove will not be called. The only function that can affect horizontal speed is PM_PreventMegaBunnyJumping in PM_Jump. Here's what it looks like:


Here pmove->maxspeed (with usp or knife) is 250 units/s, so maxscaledspeed = 300 units/s. If the speed is more than 300 it becomes equal to maxscaledspeed * 0.8 = 240 units/s (keeping the direction). Otherwise speed does not change at all!

2) 2 FOG bhop. A successful jump occurred one frame after landing. At the first frame on the ground, only PM_Friction affected us taking away 4 percent of the speed, and at the second - only PM_PreventMegaBunnyJumping. That is, at the first frame the speed will be lost in any case, but if after that it turns out to be not more than 300 units/s, then the second frame will pass without loss.

Thus, we could do without a loss of speed if we performed a bhop of 1 FOG at a speed of not more than 300 units/s. However, in practice, the large distances between the blocks and the need to change direction during bhop force us to pick up speed over 300. And then it turns out to be more profitable to make 2 FOG bhop! For example, let our speed before landing be 310 units/s. Then 1 FOG bhop will cut it to 240, and 2 FOG bhop will cut it to 297.6 units/s. Below we will correct these calculations taking into account the vertical speed and the variable fuser2, but generally the result will be the same.

While we can somehow control the speed with strafes, it is physiologically impossible to fully control the amount of FOG. Scrolling can give us a high chance of making FOG less than 3, however whether it is 1 or 2 FOG - partly player experience, partly random element. On the one hand, this dependence on luck is not very pleasant when it comes to competition. On the other hand, the unpredictability of FOG teaches the player to assume before each bhop the possibility of the worst option and to quickly make a decision after bhop. And therein lies another highlight of kreedz, because of which it became interesting to such a large number of people.

Vertical speed


Each frame reduces the vertical speed due to simulated gravity, which is specified by the cvar sv_gravity, an analogue of the acceleration of gravity. This happens in the functions PM_AddCorrectGravity and PM_FixupGravityVelocity, which essentially do the following:


Here ent_gravity can be considered equal to 1, pmove->movevars->gravity is exactly the value of sv_gravity (800 by default), and the duration of the frame in seconds pmove->frametime at 100 fps is 0.01. In total, the vertical speed inside the function will decrease by 4 units/s. If PM_Jump is not called inside PM_PlayerMove, then PM_AddCorrectGravity and PM_FixupGravityVelocity will reduce the vertical speed by 8 units/s, and then, if we are on the ground, it will be reset. If PM_Jump is called, then the vertical speed in it becomes sqrt (2 * 800 * 45) = 268.33 unit/s, and then decreases by 8 units/s due to two calls of PM_FixupGravityVelocity (inside PM_Jump and later in PM_PlayerMove).

From here it is easy to get the height of the jump. We can assume that on the ground the initial speed is V0 = sqrt (2 * 800 * 45) unit/s, the acceleration is directed down and equal to a = 800 units/s^2 . Then the height will be H = V0 ^ 2 / (2 * a) = 2 * 800 * 45 / (2 * 800) = 45 units.

Note that if fps decreases, then a smaller number of frames will be necessary for the jump, however, due to the fact that the formula uses pmove->frametime, the height of the jump will remain almost the same. If you look again at the article about strafe physics, you will find that the same principle is laid down in the functions PM_Accelerate, PM_AirAccelerate and PM_Friction.

And one more important point: when we make a jump, we have PM_AddCorrectGravity called before PM_PreventMegaBunnyJumping, so the vertical speed at this moment will not be zero, but -4 units/s. Consequently, the total speed will be more than 300 when the horizontal component is greater than sqrt (300^2 - 4^2) = 299.973 unit/s. So when jump statistics says that a good prestrafe must be less than 300 units/s, it deceives you a little.

fuser2


And now the most interesting part. In the article about HighJump physics we became more familiar with the friction in the GoldSource engine. During the evolution of Counter-Strike, the parameter fuser2 was added to this friction, which affects all three components of speed. At the moment of the jump in PM_Jump function the variable fuser2 becomes equal to 1315.789429 (looks like a magic number, but I suppose it is 100 * 1000 * 19.0 / 4.0). Then each frame in PM_PlayerMove we have a call of PM_ReduceTimers, which along with other things reduces fuser2 by the length of the frame in milliseconds (i.e. at 100 fps 10 is subtracted from fuser2). If within 1.31 seconds a new jump does not occur, then fuser2 reaches zero and no longer affects physics.

However even on a flat surface fuser2 does not have time to be reset between bhops. Because of this in PM_Jump, before the call of PM_FixupGravityVelocity, the vertical speed is truncated. The horizontal speed at the beginning of the function PM_WalkMove also changes in the same way:


PM_WalkMove is called only on the ground, and we feel it when doing normal jumps on any climb section. The less time we spent in the air, the longer we have to pause after landing before the next jump. Now you understand that the fuser2 variable is to blame for it.

Let's go back to a flat surface. First, we just run and jump. At this moment, fuser2 is zero, so at 100 fps we know for sure that the vertical speed is 268.33 units/s, and that we will stay in the air for 66 frames (we are not considering stand-up bhop yet), and that the maximum jump height is 45 units. Suppose that by the end of the flight we reached a speed of 310 units/s. As it was found earlier, it would be more profitable for us to make 2 FOG bhop. Let's check if this result changes with the account of fuser2.

At the first frame on the ground PM_Friction will take 4 percent from the horizontal speed, leaving us 297.6 units/s. After 66 frames in the air and 1 frame on the ground fuser2 =1315.789429 - 67 * 10 = 645.789429, therefore after PM_WalkMove the horizontal speed will become equal to 297.6 * (100.0 - 645.789429 * 0.001 * 19.0) * 0.01 = 261.08 unit/s. At the second frame inside PM_PreventMegaBunnyJumping, we find that 261.08 < 299.973, so the horizontal speed will not change. In the case of 1 FOG, the speed would be cut to 299.973 * 0.8 = 239.98 units/s, so 2 FOG in this case really remained more profitable. By the way, if we did DropBhop, during which we had to pick up a fairly large horizontal speed, then 3+ FOG bhop could be even more profitable.

As for the vertical speed, at the second frame on the ground fuser2 will decrease by another 10, and the vertical speed before calling PM_FixupGravityVelocity will be 268.33 * (100.0 - 635.789429 * 0.001 * 19.0) * 0.01 = 235.92 unit/s. Then, after bhop we will spend 58 frames in the air, and the jump height will be just 34.78 units. At the next bhop, depending on fuser2 and the number of FOG, we will be in the air for about 56-58 frames.

If we used a stand-up bhop instead of the usual one on a flat surface, we would have spent more (about 64-65) frames in the air between jumps cause of ducking. Due to this, fuser2 would have time to decrease more, and jumps would be about 1 unit higher, and the speed in the case of 2 FOG bhop would not decrease so much. However, this does not mean that stand-up bhop is always better than usual one, because it takes more time. That is why experienced players often try to do a regular bhop instead of a stand-up if possible.

We will return to the topic of stand-up bhop and explore the work of duck in the next article.