Last server records
Pro Nub

EdgeBug and JumpBug physics

Posted by Kpoluk 27 Jan 2019 in 12:20
When I insert code snippets into an article, I'm trying to show only what is relevant for a discussed subject. Therefore, in the articles about Bhop and CountJump physics we ignored the function PM_CategorizePosition for simplicity, but in this article it will play a very important role. To begin with, we will clarify exactly where it is called:

  • in PM_PlayerMove before PM_Duck;
  • in PM_PlayerMove after PM_WalkMove and PM_AirMove;
  • in PM_Duck in case of successful ducking;
  • in PM_UnDuck in case of successful getting up.
Here's what happens in it:

As you can see, PM_CategorizePosition makes a trace 2 units down and checks if it has touched anything. If there are no intersections, then all the components of tr.plane.normal are zero, and we are in the air. If the trace touches an object, then the normal to the surface of this object is written into tr.plane.normal. Let x be the angle between the normal and the vertical axis, then the condition tr.plane.normal[2] < 0.7 can be rewritten as follows: cos(x) < 0.7, hence x > 45.573°. That is, if the slope of the surface is more than 45.573°, then we are dealing with a slide, which from the point of view of the variable pmove->onground is equivalent to being in the air.

Further, if the touched surface is not a slide, PM_CategorizePosition teleports us to it (unless of course we are not already standing on it). Due to this, when descending at normal speed along a not too steep slope, we are not constantly appear to be in the air, but calmly walking along the ground. A little later, we will return to this seemingly harmless function.

Changing origin

In the article about strafe physics we considered the change in velocity in the functions PM_AirMove and PM_WalkMove, while the change of coordinates was left out. Now it's time to fix it.

1) At the end of PM_AirMove right after obtaining of new velocity PM_FlyMove is called. It not only gets new coordinates when moving in the air, but also can handle collisions with surfaces, including several at the same time. However, in this article we will be interested in the interaction with only one surface, so that the function can be significantly simplified:

Taking into account the current velocity, we get the end point, in which we would like to be, and we make a trace from the current position to the end. In the case of free flight this is where everything ends, so end just becomes our new origin. If the trace touched some object, we move to the point of intersection with the object's surface, then get the velocity after the collision in the function PM_ClipVelocity and repeat the iteration for the rest of the way, only this time the trace will be be made from a new point, taking into account the new velocity vector. Plus, when calculating the new end point, we should also keep in mind that part of the path has already been passed, which means that the time time_left remaining until the end of the frame has become less.

In total, PM_FlyMove can perform up to 4 iterations, that is, during the frame it can handle up to 4 consecutive collisions with different surfaces. It remains only to understand how PM_ClipVelocity changes the velocity vector in a collision:

Denote V as the original velocity vector in, Vnew as the final velocity vector out, N as the normal to the surface. Then the formulas from PM_ClipVelocity are reduced to the following form:

That is, in fact, we get rid of that part of the velocity, which is perpendicular to the surface. If a component parallel to the surface was zero or just too small, this would mean that we would just stop after the collision.

2) when moving on the ground we get obtain coordinates in PM_WalkMove:

As in PM_FlyMove, here is the receipt of the dest point which we would like to get to. The only difference is that we ignore the vertical speed i. e. if the trace to dest does not hit anything, then we will move only in horizontal plane. This is enough for us in this article, so I don’t provide further code, but for the sake of completeness, I’ll briefly describe what is happening there.

So, we are on the ground and we are trying to move in the direction of the horizontal velocity, but we hit something. Here two different situations are possible: when we go up the slope and when we have a stair. In the first case, it's enough to call PM_FlyMove, but in the second case everything happens more cunningly - the player rises to a height equal to the value of the cvar sv_stepsize (18 units by default), PM_FlyMove is called, and then the player is lowered by the value of sv_stepsize. Of course, raising and lowering are done carefully, with a preliminary trace. It is not known in advance which of the cases we face, so the developers solved this problem in the following way - a position prediction is made for both situations, and then the one that is farther from the current position is selected. Thanks to this approach, we can not only climb the slopes, but without additional actions climb stairs up to 18 units height, which we already mentioned in the article about CountJump physics.


Now we are ready to understand how the landing works. And we are not only interested in the frame at which we fell to the ground, but also in the one that follows it. In the pictures we will repersent them separately, so that you can see which of the frames is responsible for the processes occurring between them. Let's conditionally divide the landing into 4 types:

Type 1. we fall on a horizontal surface, and after another call of PM_FlyMove in PM_AirMove it turns out that the distance from the model to the ground is less than 2 units, which means after PM_AirMove there will be a call of PM_CategorizePosition, which teleports us to the ground. In the next frame PM_WalkMove will be in charge of the origin change, moving us along the ground along the horizontal velocity component (the vertical will simply reset).

Type 2. we fall on a horizontal surface, and another call of PM_FlyMove handles collision, projecting the speed on a horizontal direction. Next we have a call of PM_CategorizePosition, which with help of pmove->onground confirms that we are on the ground, and therofore PM_WalkMove is called at the next frame. The final velocity will be the same as for Type 1.

Type 3. we fall on a slope that is not a slide, and just as in Type 1 we are teleported on the surface, so the call of PM_WalkMove resets our vertical speed at the next frame. If the horizontal component in this case was non-zero, then after PM_WalkMove we find ourselves in the air. And if the fall was strictly vertical, then after the collision we would completely stop!

Type 4. we fall on a slope that is not a slide, and just as in Type 2 PM_FlyMove handles collision, leaving only that part of the velocity which is parallel to the surface. Then PM_WalkMove will reset the vertical component of the new velocity and throw us away from the surface.

Note that the velocity here behaves completely differently than in Type 3. If we fell vertically with speed V on a plane with an inclination of 45°, then after the collision the speed would be equal to V * cos(45°) * cos(45°) = V / 2, plus PM_Friction would take its 4%. For example, with a maximum fall speed of 2000 units/s (defined by cvar sv_maxvelocity) we would fly away at a speed of 960 units/s. Collision of this type can be called as bounce.

Demos kz_42_amazon_surRendi_0228.26 and kz_j2s_darktower_shooting-star_0102.77 serve as good examples of using bounce for gaining high speed before pressing the start button.

Fall without losing HP

And now we've reached the main topic of the article. There are several ways to fall from a great height without losing HP, but the essence will be the same every time - the interaction with the ground occurs somewhere in the middle of one of the frames, while at the beginning and end of this frame we are in the air, thus a function that is called after PM_PlayerMove won't subtract any HP.

1. EdgeBug. Landing takes place on the edge of the block.

The greater the horizontal speed, the greater the chance that at the end of the frame we will not stay on the ground. At that the landing here is of the Type 2, with a change in direction of movement due to the call of PM_FlyMove.

If you want to see the use of EdgeBug during run on a map, you can check the demo kz_giantbean_b15[1337trees]_spr1n_0103.97. Another good example is fof_32_shooting-star_0125.92, where EdgeBug was done with minor horizontal speed on a moving object.

2. JumpBug. As we found out, the function PM_CategorizePosition teleports us to the ground if the distance from the feet to the ground is less than 2 units or, what is the same, if the center of the standing model is 36-38 units above the ground. The idea of JumpBug is that at the end of the first frame we find ourselves in this gap, but in a sitting position, so that the teleport does not occur, and at the next frame we simultaneously get up and jump. Thereby we have a call of PM_CategorizePosition inside PM_UnDuck, i. e. at the end of the second frame we are finally teleported down, but then the function PM_Jump puts us in the air again. If PM_Jump was called in the code not after, but before PM_UnDuck, then we would be unable to do any JumpBug at all.

Thus, here we are required to firstly perform the fall of the Type 2, and secondly, to simultaneously get up and jump in a certain frame. The jump can be done either by pressing a button or by scrolling. Getting up is usually performed by releasing the duck button, which was pressed during the flight. However, as we remember from the article about CountJump physics, the call of PM_UnDuck also occurs one frame after the scroll duck. So, in theory, you can perform a JumpBug using a scroll only: just do a scroll duck at the first frame and a scroll jump at the next frame.

Horizontal speed before a jump does not matter. It is only necessary to understand that since PM_Jump is called, then quite a lot of speed will be cut by function PM_PreventMegaBunnyJumping, as was discussed in detail in the article about bhop physics.

As an example you can watch demos kz_ep_gigablock_b01_kayne_0206.57 and kz_man_redrock_shooting-star_0527.35, as well as famous kz_cg_wigbl0ck_spr1n_0211.50 with three JumpBugs in one run. Besides, in kz_giantbean_b15[1337trees]_spr1n_0103.97 mentioned earlier Estonian jumper spr1n performs JumpBug on invisible block right after EdgeBug!

3. DuckBug. In the case of the fall of Type 3, we could also do a JumpBug, but since we already find ourselves in the air due to the impact from the slope, it's not necessary to do a jump. It means that you just need to get up at the right moment, however when falling from a given height, this moment can vary depending on which point of the slope we land at. That's why jumpers tend to use scrolling for DuckBug (for example, as in the demo kz_6fd_volcano_kzz1lla_0103.42), although regular release of duck button works here as well.

4. SlideBug. As we saw earlier, from the point of view of pmove->onground variable, there is no difference between being in the air and on the slide, so the EdgeBug can be slightly modified by replacing the fall on the edge of the block with a landing near the slide bottom. For instance, SlideBug can be used on kz_cg_wigblock (download demo).

Most likely these are not the only possible options, so if you want, you can come up with your own technique.

Fall height

Falls of Types 1 and 3 are fundamentally different from Types 2 and 4 in that at one of the frames the vertical coordinate of the player in a standing position is at a distance of 36-38 units above the ground. If the fall height is known, then with a stable value of FPS we can say for sure whether this condition is fulfilled, and therefore whether it is possible to perform one of the techniques described above. To do this, we could step by step obtain height values ​​using formulas from PM_FlyMove to find the coordinates and from PM_AddCorrectGravity for velocity change. However, it will be better to obtain universal formula that would immediately give an answer.

Suppose that in the process of falling we have a stable 100 FPS, and the initial vertical speed is zero. Then the frame length is pmove->frametime = 0.01 seconds, and the speed at each frame is reduced by 8 units/s. One frame later passed height is 0.01 * 8 units, after two frames it's 0.01 * 8 + 0.01 * 8 * 2, after three frames 0.01 * 8 + 0.01 * 8 * 2 + 0.01 * 8 * 3 and so on. N frames of falling give us

h = 0.01 * 8 * (1 + 2 + ... + N) = 0.04 * (N + 1) * N

If we know the height H of a certain place on map, then we can find number of frames by solving quadratic equation:

H = 0.04 * (N + 1) * N
N^2 + N - 25 * H = 0
N = (sqrt(1 + 100 * H) - 1) / 2

Since N must be an integer, we take the nearest integer less than N, denoted as [N]. After [N] frames we will pass the distance h = 0.04 * ([N] + 1) * [N]. It remains only to find the difference H – h and check whether it is less than 2 units.

Suppose we want to make JumpBug falling from a certain height, but the calculation showed us that this is impossible. In this case, we can try to change the height by making a jump. For example, a jump from a place will add 45 units to a height, dd will give an additional 18 units, and a jump from a sitting position 45 - 18 = 27 units. Since dd does not affect fuser2, cj and dcj will also add 45 units, and duckbhop after dd 27 units. You can use the regular bhop, but then the height will depend on the FOG and fuser2, as we already seen in the article about bhop physics. At that after the first bhop the height will be around 34.5-34.8 units, and after the second and further 33.1-33.4 units. For stand-up bhop we will get 35.5-35.8 units after the first bhop and 34.5-34.8 after the rest. Using duckbhop, if we start from a standing position, then before the first bhop we spend as much time in the air as with a stand-up, so the numbers will be 18 units less, that is, 17.5-17.8 units. Between subsequent bhops we are in the air as many frames as in the usual bhop, so we get 15.1-15.4 units. A good example of varying height can be found in a demo already mentioned before - kz_cg_wigbl0ck_spr1n_0211.50: first JumpBug made after bhop, the second one after the usual jump, and the third one after dd.

It is quite tiring to do calculations for each of the possible heights, so later in a special article I will talk about the plugin, which will give you a hint whether JumpBug is possible from a certain place. In the meantime, let's estimate how frequent are heights from which you can do a JumpBug.

As long as the fall speed is less than 200 units/s, we fly down less than 2 units per frame, therefore, we can make a JumpBug at any frame until reaching this speed. By the way, we will reach it after N = V / 8 = 200 / 8 = 25 frames, having flown h = 0.04 * (25 + 1) * 25 = 26 units. At speeds higher than 200 units/s the desired ratio P can be determined as 2 units divided by the distance traveled per frame. For example, at a speed of 400 units/s, reached at a height of h = 0.04 * (50 + 1) * 50 = 102 units, we get P = 2 / 4 * 100% = 50%. And with a maximum speed of 2000 units/s, reached at a height of h = 0.04 * (250 + 1) * 250 = 2510 units, we have P = 2 / 20 * 100% = 10%. That is, starting with 2510 units at one height, from which JumpBug is possible, there will be 9 heights from which it is impossible (but you can make an EdgeBug from them). In the interval 26 < H < 2510 the dependence of P on h is found as P = 2 / (8 * N * 0.01) * 100% = 50 / (sqrt(1 + 100 * h) - 1) * 100%. On the graph this can be conventionally depicted as follows:

Here we should recall that we are doing JumpBug to save our HP. Losses start from a height of 164 units (P = 50 /(sqrt(1 + 100 * 164) - 1) * 100% = 39.3%), and you can die with 100 HP if you fall from a height of 603 units (P = 50 / (sqrt(1 + 100 * 603) - 1) * 100% = 20.4%).

Engine FPS

In theory, even small deviations in the FPS should greatly influence the height from which you can make a JumpBug. However, this is not really the case. Let's see how the pmove->frametime variable used for getting new coordinates is calculated in PM_PlayerMove right before PM_ReduceTimers:

Here pmove->cmd.msec is the frame duration in milliseconds (integer). If we try to get the FPS value (let's call it engine FPS) as 1.0 / pmove->frametime = 1000.0 / pmove->cmd.msec, then with a frame duration of 10 milliseconds the FPS will be 100, and the neighboring values 9 and 11 milliseconds will give 111.11 and 90.90 FPS respectively. This means that as long as our real FPS is more than 90.90 and less than or equal to 100, the engine FPS will be exactly 100.

For the same reason, in the introduction to this series of articles special attention was paid to the cvar fps_max. If you exceed the legal value by 0.5, then the real FPS will not differ so much, however the engine FPS will become equal to 111, which in turn will affect not only the collision with the surfaces, but also how speed is gained in the functions PM_Accelerate and PM_AirAccelerate (recall that pmove->frametime is also used there).

In the next article PM_FlyMove will also play an important role.