Last server records
Pro Nub

Ladder physics

Posted by Kpoluk 27 Jan in 18:53
Moving along ladders is not something sophisticated at first glance, but we can only figure it out now, having an idea of ​​the internal structure of the map. The very fact of being on a ladder in PM_PlayerMove is determined in the function PM_Ladder, which essentially checks whether the player's center is inside the hull of some brush entity with CONTENTS_LADDER. That is, to move along the ladder, you need to at least be inside it for a little bit. Movement inside the ladder is defined by the following function PM_LadderMove, which transforms the desired player speed so that he moves along the ladder.

From this we can conclude that the distance the player moved inside the ladder depends on his speed at the frame before crossing the hull of the ladder, as well as on which side the player enters the ladder. For example, if we try to jump onto the ladder on kz_longjumps2 (or kz_longjumps3) by simply running towards it, we will end up inside the ladder and the closer to the wall the higher the speed (even at 340 units/s we will move no more than 3.4 units inside, while the thickness of the ladder on these maps is 4 units). If we approach the ladder from the side along the wall, we will find ourselves inside the ladder and right next to the wall. This is a good life hack for those who want to conquer ladder blocks.

Let's now take a closer look at what happens to the velocity vector when moving along (inside) the ladder. As usual, we will transform PM_LadderMove to a more digestible form and analyze its operation step by step:


First, based on the dimensions of the ladder, ladderCenter is found , and regardless of the slope of the ladder, the center will lie inside it. Next, PM_TraceModel finds the intersection of the line connecting the player's center and the center of the ladder with the hull of the ladder, where the hull is taken for a point object, that it actually repeats the shape of a brush entity. The result of the intersection is written to the trace. If there is no intersection, we do not change the velocity and exit the function, otherwise we move on. If the jump key is pressed, then we set the speed equal to 270 units/s and direct velocity along the normal to the plane of the ladder obtained in the trace, and again we exit. If there was no jump, we begin the calculation:


Here our old friend AngleVectors calculates two unit vectors in the direction of view: v_forward looks in the direction of our sight, and v_right to the right relative to us. Depending on the keys pressed, the components forward along v_forward and right along v_right are equal in magnitude to MAX_CLIMB_SPEED (i.e. 200) or zero, so the resulting speed will be either zero, or equal to 200, or approximately 200 * sqrt(2) = 282.8427. If the crouch key was pressed, these values ​​are multiplied by PLAYER_DUCKING_MULTIPLIER (i.e. 0.333), decreasing approximately by 3 times. Now all that remains is to project the velocity vector onto the plane of the ladder, but the CS engine does this a little more originally than you might expect:


Vn is found as the projection of the velocity vector onto the direction of the normal trace.plane.normal to the plane of the ladder, and Vl is the component of the velocity V lying in the plane of the ladder. To obtain the resulting velocity vector pmove->velocity, the product of Vn and the unit vector tmp is unexpectedly added to Vl. In the simplest case of a vertical ladder, tmp is directed upwards, and if the ladder is tilted, it will tilt along with it. But let's not rush things and try to move along vertical ladder:


1) look up along +Z, press W. The desired velocity vector V is directed vertically upwards, Vl = V, Vn = 0, which means the result of pmove->velocity matches V, and we move upwards at a speed of 200 units/s.
2) look sideways at +Y, press W. The desired velocity vector V is directed sideways, Vl = V, Vn = 0, again pmove->velocity matches V, we move sideways at a speed of 200 units/s.
3) face the ladder, look at +X, press W. The desired velocity vector V is directed along +X, Vl = 0, the projection of the velocity on the normal to the ladder Vn = –V, and then pmove->velocity = 0 - tmp * Vn = tmp * V = (0, 0, V), that is, we move up at a speed of 200 units/s, just like in the first case!

Any speed can be broken down into the three components considered, which means the overall picture becomes clearer: everything that lies in the plane of the ladder remains unchanged, and the component directed towards the ladder becomes directed upwards (accordingly, the component directed away from the ladder becomes directed downwards). But where should the desired velocity vector be directed in order to move up as quickly as possible? Let's consider the following case: 4) turn to face the ladder, look along +X, raise your sight to the angle u, press W.


The desired velocity vector V = (V * cos(u), 0, V * sin(u)), the projection Vn = -V * cos(u), the component Vl = (0, 0, V * sin(u)), the resulting velocity pmove->velocity = (0, 0, V * sin(u)) + V * cos(u) * (0, 0, 1) = (0, 0, V * (cos(u) + sin(u))). The maximum vertical speed will be obtained at u = 45°, amounting to 200 * sqrt(2). You can try looking down, that is, taking negative values ​​of u, there we will find that at u = -45° we get zero speed. So, in general, the picture is as follows: we look down, the speed is 200 units/s down, we start to look up, the speed of descent decreases and after 45° we freeze, then we start to move up. When the sight has risen to the horizontal, the rate of ascent has reached 200 units/s, we look even higher and after 45° we reach the maximum of 282.8427 units/s, after which we slow down and again reach 200 units/s, looking up.

Now recall that by holding down two keys at once, for example W and D, we can create even greater speed. If we continue face the ladder, then holding down D will give additional movement to the right. But we can turn to the ladder with our right side, and then the entire component of the speed from pressing D will turn into the speed of movement upward. In order for the component from pressing W to also go into vertical velocity in this position, it is enough to look up. Then we get pmove->velocity = 2 * 200 = 400 units/s. Thus, we got the maximum speed of ascent and descent on the ladder, well known to all experienced players.
What about the maximum horizontal speed? Let's take the ladder on kzro_nightcamp as an example, where you need to move along the wall. Here, you can't use the horizontal-to-vertical conversion, so the maximum speed here is 282.8427 units/s, which can be achieved by holding W and A and turning 45° to the wall, or W and D and turning 45° away from the wall.

Llet's consider a ladder tilted at 45° (an example can be found on kz_alpin). The tmp vector will also be tilted by 45° here, which means that in order to reach the speed of 282.8427 units/s with W pressed, it is enough to look at the horizon towards the ladder. The option with W and D pressed to reach 400 units/s also works here, in which case you need to look at the horizon, turning to the left by 45°.


With smaller tilts, maintaining the position of the desired velocity relative to the plane of the ladder will require looking up and turning away from the ladder less, with larger tilts it is the opposite. The situation changes only when the ladder completely lies in the horizontal plane, then the tmp vector degenerates and becomes zero. Moreover, it doesn't matter whether the ladder is under your feet or over your head. Such a ladder can be found, for example, on vee_mojave, and here, as on kzro_nightcamp, the only option is to hold down two keys and look at the horizon, turning sideways at 45° and moving at a speed of 282.8427 units/s.

Let's return to PM_LadderMove, in which it remains to check whether there is a need to leave the ladder. To do this, we check if there is a floor 1 unit away under our feet, and also look at the Vn sign.


If a floor is found, and there is a velocity component directed away from the ladder, then a vector of 200 in the direction of the normal to the plane of the ladder is added to the current velocity, i.e. something like a push off from the ladder occurs. This feature is also used in modern mapping, providing the ability to get 200 units/s when leaving the ladder.

Another well-known fact related to the transition between the floor and the ladder is that it is impossible to start climbing the ladder while sitting on the floor. Now this also becomes quite obvious: the maximum speed on a ladder in a crouch will be 400 * 0.333 = 133.2 units/s, which is not enough to successfully lift off the floor. Indeed, as we remember, every frame the engine uses the PM_CategorizePosition function to check if the player has ground 2 units under his feet, and if it is there, the player is teleported to it, i.e. you can lift off the ground only with a vertical speed greater than 200 units/s.


Ladder boost


A phenomenon called ladder boost is worth discussing separately. The player somehow climbs deeper into the ladder, after which he is thrown to the other end of the room so quickly that it looks more like teleportation than a boost. Examples are the shortcuts performed on the maps cg_d2block_ez and kz_wild. Let's figure out what's going on here.
Let's go to the vertical ladder standing on the horizontal floor, and, holding W and looking down, we'll bump into it. As we've already found out, the desired speed will be completely redirected downwards, after which in PM_PlayerMove we'll get to the already familiar PM_FlyMove function. There, the vertical velocity is projected onto the plane of the floor, thereby being zeroed out. However, the floor may not be horizontal, but inclined, so if after the ladder the velocity vector rests against slope that goes down, then after projection the velocity becomes directed along the floor, i.e. inside the ladder.


If the ladder is thick enough, then we will dive into it further and further. And as soon as the player's center is inside the brush entity, the intersection detection breaks down. To see this, let's follow the entire chain. In PM_LadderMove an instance of the trace structure trace_t is created. At this point, it contains some garbage. Then it is passed to PM_TraceModel, which in turn passes the trace to SV_RecursiveHullCheck, that searches for a specific intersected plane. At the same time, unlike PM_RecursiveHullCheck, the SV_RecursiveHullCheck function assumes that an intersection will definitely be found (after all, in PM_Ladder we learned that we were inside a ladder, and no one expected such a deep dive into it), therefore, when after going through all the planes SV_RecursiveHullCheck does not find anything, it simply leaves the trace in the state in which it was passed to it, that is, with some garbage inside. Thus, PM_LadderMove gets huge values ​​of the trace.plane.normal components, from which the huge pmove->velocity is obtained, and in the next frame the engine throws us across the entire room, or even the entire map. The trash values ​​are different each time, but in relative terms this change is not very big, so it throws the player in approximately the same place. On the other hand, there is no guarantee that after the next CS update the values ​​will not become completely different, so intentional use of ladder boost on your maps is strongly discouraged for mappers.