Welcome to the Doom Movements Bible! In the second part, as in the first, all the quirks and whims of the movement code in Doom, including intricate stunts describing their work, are sorted out and categorized.
SlideMove: slippery moments
Up to the present moment, all the considered tricks with the character's movement in the game relied on the discrete nature of movement in the DOOM engine. Now we will dig the game code much deeper, and get to the function with the innocent name P_SlideMove. The function contains a comment warning the careless reader of the code that there is “total confusion” in front of it. I don’t know whether John Carmack or Bernd Kreimeier added this commentary, but it’s very, very accurate.
WALL RUNNING
We have already analyzed how a large SPT (“State of Attempted Teleportation”, which can be perceived as a directional impulse of movement of the character, discussed in Part 1) can lead to bugs, for example, line skip (crossing the border). Fortunately, the game engine is not so stupid and tries to take into account such errors! So, if the bugs start, if the character moves too fast, the perfect solution would be to simply divide one move into several suitable sizes, wouldn't it? And this is exactly what the game engine does: if the X or Y vector of movement exceeds 15 units (which, as you can see, is one unit less than the player’s radius), the engine tries to divide the player’s movements into two parts in order to calculate each of them separately. Thus, if the speed is too high, the program checks if the character crosses the obstacle in the "intermediate" state and, if necessary, does not miss it. Since the program conducts two movements instead of one, we will call this process “double SPT”.
“Wait a minute!” - the reader will be indignant here. - “Recently you talked about crossing borders at SPT> 15, and now you are describing a code that does not allow you to do this! How can it work at all? ” Well, then let's just look at the code: ')
f (xtap > 15 OR ytap > 15)
Did you notice? Well, what happens when a character moves south or west? X-, as well as Y- suddenly appear ... Negative! And, for unknown reasons, the game engine will not verify this curious fact.
But back to the main idea. So, the engine uses the SPT to determine where to "teleport" the character in the next step. If the “drop zone” is occupied, the game code calls the P_SlideMove function, whose task is to check the fact of a collision with an obstacle and, if that happened, to ensure the character’s movement so that the collision looks natural. To start, the engine determines the direction of the player’s movement and calculates three vectors emanating from the three leading corners of the character box. Then it checks each vector for collision with an obstacle. If a collision is detected, the program determines the percentage of the path to the obstacle and makes the character move to the point found. And then he takes the rest of the SPT, directs it parallel to the wall, which the player hit, this part of the movement is the “slide” along the wall.
And it works, more or less. There is a small internal problem hiding here: to calculate the “slide”, the subroutine uses the initial SPT of the character, even if it was divided into two separate movements due to the player’s high speed.
Now that we know this, let's step through the entire process:
the player runs against the long horizontal wall
that a player’s SPT, for example, is X- = 20 and Y- = 2
the engine determines that X-SPT is too large, and divides the movement into two, each with impulses X-SPT = 10 and Y-SPT = 1
now the engine checks the first of two movements and determines the collision with the wall to the north of the player
P_SlideMove comes to the rescue and draws three vectors from the character's "corners" directed north and east. But for the calculation of the vectors takes the base SPT, that is, X-SPT = 20 and Y-SPT = 2 (and not X-SPT = 10 and Y-SPT = 1, as it should)
vector crosses the wall in the north
the engine moves the character, for example, by 5% from the current SPT
and, finally, the engine redirects the remaining 95% of the base SPT along the wall, moving the player to 19 units.
after which, satisfied with the work done, the engine proceeds to the second half of the player’s movement, performs the usual procedure and sends the character another 19 units to the east.
To summarize the above: if a player moves north or east with a sufficiently high speed (X-SPT or Y-SPT exceeds 15 units), the initial movement attempt is blocked by an obstacle, the engine performs two complete moves in one tick! Note that as a result, the SPT or something else does not change; it just happens two moves during one.
THING RUNNING
trick at 0:12
Now that we have mastered “wallrunning” (“running against the wall”), let's think about it: what if we replace the wall with any impassable object, and better with a long line of such objects?
The engine will try to teleport the player to a new position and detect an obstacle in the landing zone. Then the program will call the P_SlideMove known to us, and she will draw three vectors from the corners and find out: there is no collision. And what will the engine do now?
Strangely enough, the specified function does not actually check the character's collision with objects, it only checks the intersection of the boundaries of the objects with the vectors drawn by it. And if P_SlideMove could not find the line blocking the movement, it uses its last secret weapon: “stairstep” (running by a ladder). The program tries to move the player exactly along the Y axis, and if it could not, along the X axis. Let me remind you that all the objects in DOOM are enclosed in a “box” with the sides parallel to the axes of coordinates. Thus, if a player moves along an obstacle and the stairstep program decides to move it horizontally, the player will slide along one side of the object.
Now, having put the above in mind, it is easy to understand that the thingrunning is exactly the same as wallrunning: the player moves north or east with “speeding”, the movement test divides each movement into two equal parts, and each half gets the full initial vector PST with a generous P_SlideMove feed (as long as the initial landing point is blocked). The character successfully makes two complete movements instead of one for each tick, simply sliding at double speed along the “edge” of objects. This trick is best known for its use on Map23, where a player can accelerate by doing thingrunning along a number of barrels, and as a result, jumping over part of the map in a seemingly impossible way.
WALLRUN "AIR CONTROL"
trick at 10:17
When I first saw this trick, he stunned me so much that I decided to write an article about the magic of the DOOM engine - the very guide that is now before your eyes.
One of the easiest and most understandable rules for a player to move to DOOM is: “air control does not exist!” This means that when a character’s feet do not touch the ground, the character’s movements depend only on local laws of physics, but not on the player. And how I was amazed when, when viewing the Map14 speed crane, I saw that the player performed wallrunning high in the air, ran along the wall directly to the exit, and at the end simply turned above the ground and headed towards the south door, although before that it was just running east.
At first, I just couldn’t realize how such a trick is possible - the player's PTA was directed exactly to the east! He had zero Y-TSP, how could he start going south! But, as always, the seemingly understandable code contains surprises and dirty tricks.
It turns out that if the player is close to the obstacle, the engine will not try to bring the player closer to the barrier. He will simply use his last resort, the already familiar “stairstep”. Here we get to know the magic number 3.125%. If one of the vectors drawn by the program from the player’s corners crosses the wall in less than 3.125% of its length, the program does not even attempt to make the player move in the initial direction. Skipping all the extra steps, the engine will use the "stairstep". Well, the code of the ladder, as we remember, is able to move the character strictly horizontally (or vertically), without at the same time not changing the initial SPT of the player.
Now that we have reviewed the logic of the process, the incredible “turn in flight” can be explained. The player "goes for takeoff", pressing "up" against the wall, while having a high X-SPT and a small southern Y-SPT. Each tick, while the player is above the ground, the engine tries to move the character to the east and a little to the south, but detects a wall at the end point of the move. The engine instantly gives up and calls for help "stairsteps" and sends the character to the east, while not changing the vector of SPT in any way. As a result, Y-TSP remains slightly negative throughout the “air run”. And in the final, when the passage to the exit is reached, the movement check is successful, and the player changes the direction of movement in the air, which would seem impossible in the realities of DOOM physics.
MOMENTUM PRESERVATION
This trick is not as obvious and entertaining as described above, and it was difficult for me to find a suitable name for it. I have heard the name “door trick”, and it is very often used by speedrunners while waiting for the door to open.
The trick is trivial - resting the impassable wall in the right way, the player can keep the accumulated impulse of movement (SPT) at the maximum value, remaining motionless (the result is easy to detect visually, observing the fast oscillation of the weapon in the player's hands, as at full speed of the race. Therefore this trick is called “ wobble glide "- swaying glide). With a speedran, this means that the player maintains the maximum speed while waiting for the door to open, and when the passage becomes large enough, the bullet rushes further, saving precious milliseconds.
But how does this trick work? Usually, if a character runs into a wall, its speed (its SPT) is instantly reduced by the engine. This can be detected by changing the animation of the weapon in the hands of the player. To understand the nature of what is happening, we will again have to delve into the code of the DOOM engine and understand how it defines collisions.
Recall that usually when encountering a wall, the subroutine P_SlideMove known to us calculates the percentage of the motion vector directed into the wall (or skips this part if the collision is found at a distance of less than 3.125% of the vector), and then redirects the “remainder” of the movement parallel to the wall. In a normal situation, this quite reasonable algorithm will quench your speed - if you hit a wall run at a right angle, the slip speed will be proportional to the cosine of the angle difference between the direction of movement and the direction of the wall, and cos 90 is zero.
But let's look deeper. How exactly does the engine determine the future collision with a wall? To solve this problem, the program solves a simple mathematical equation: take a line representing an impassable wall, take a motion vector directed from one of the corners of the character box and find the intersection point. These calculations can be made using vectors, matrices, and other mathematical tools. But the DOOM engine uses the simplest method: take the end points of segment A and check which side they are relative to segment B. Then we check which side of A are the extreme points of segment B. If in both cases the extreme points are on different sides of the lines, the segments intersect.
To perform such a simple test, we need a little division and multiplication. And here the game engine reveals another aspect. The engine stores all data in the form of 32-bit values, and, most importantly, in the form of numbers with a fixed comma. In this case, one bit is used to indicate the sign of the number (positive / negative), 15 bits store the integer part and the remaining 16 bits are fractional. 15 bits is quite a bit; in fact, the allowable range of values ​​is from -32768 to 32768. So, if you need to multiply two numbers, you have to be very careful that the result is not out of range. The square root of 32768 is just 181, which means that checking the intersection of two lines of 200 units each will overflow the memory cell. And how to cope with such a puzzle?
Here is a part of the code from the “crossing check” function that copes with the problem:
left = (line->dy / 256) * (dx / 256); right = (dy / 256) * (line->dx / 256);
As you can see, it simply divides all values ​​by 256 before the start of calculations (to be perfectly accurate, it performs a bitwise right shift by 8, although this does not change anything). In this simple way, the engine ensures that the result of the calculations will be small enough to avoid overflow. At the same time, we reduced the fractional part from 16 to 8 bits, so there are no problems, is it?
Well, in most cases there are no problems. But what happens if the SPT in one of the directions is extremely small? For example, a player runs into a corner, having an X-TPS 20 and a Y-TFS 0.001. Let's take a closer look:
the engine tries to move the character 20 units east, and by 0.001 units north, but cannot, as it detects a wall in the north
the engine calls P_SlideMove to handle collisions, draws three vectors from the corners
the engine checks the first vector and detects the intersection with the wall in the north, as intended
the engine checks the second vector, but due to division by 256, the tiny 0.001 Y-CPT is already rounded to zero, and the program does not find the intersection of the vector with the wall in the north
then the engine tries to make a partial movement of the player in the direction of the first vector, for example, by 50% of the vector size
but when the engine tries to move the player to 0.0005 units in a northerly direction, it fails, finding a wall at the end point
the engine helplessly lowers its hands and causes a “stairstep”, which, as we know, in no way changes the initial SPT.
To sum up: if a player runs, resting on a corner, with an extremely small Y_SPT or X-SPT, he can maintain a high SPT, as a result of an erroneous zeroing of the vector.
VOID GLIDE
trick: 0:20
And now it's time to withdraw the heavy artillery. Instead of breaking the conventional lines of separation, we will ignore the absolutely impassable lines.
At first glance, this trick is simply impossible. To pass through the wall, the character must instantly move at least 32 units. Then, even after dividing into two parts, each “half” will be> = 16 units (character size). We already know that the maximum speed that can be reached with the use of the SR50 is 23.57 units per tick, but even if the limit is reached, the engine will split 23.57 into two separate movements. Moreover, the game code contains a hard speed limit of 30 units, which is applied at the very beginning of the movement calculation. And how in such conditions to achieve the speed of movement of the character to 32 units in one movement?
In general, it is true that 23.57 is the maximum speed achievable with conventional character controls, but what if “conventional means” is not the only way to increase the SPT? Armed with this knowledge, you can squeeze out much, much more from the DOOM engine. (I will not intentionally describe the damage boosts - it is difficult to control and test. Of course, you can get a speed of more than 23.57, for example, by undermining yourself with a rocket, but there is a much simpler and safer way).
It's time to go back to P_SlideMove. As I already described, in a collision, after performing a partial displacement in the direction of the vector, the rest of the SPT is redirected parallel to the obstacle. So, how is this piece of logic implemented? The following three steps are performed:
determine the amount of unused TSP
create a vector of the same size, directed along the obstacle
divide the new vector into the x- and y-component to determine the new X-SPT and Y-SPT of the player.
All these calculations are very simple to perform using sines and cosines. But it is here that the code seems to forget about the existence of trigonometry and uses just a brilliant simplification. Instead of determining the value of the remainder of the SPT correctly, it calls the function named P_AproxDistance. And what does this P_AproxDistance (Approximate Distance) do?
return (the longer of XTAP or YTAP) + (half the shorter of XTAP or YTAP)
Yes, yes, you were not mistaken. We do not need sines and cosines, square and cubic roots are not needed; addition is enough for us. Just add one axis to half the second axis. You can imagine how approximately this "aproximation" (approximation).
Thus, the function gives the player a very serious bonus. The fact is that the result of such a calculation is consistently more than the correct result. Let's check with an example: calculate the player's “slip” vector when X- = 3 and Y- = 4. The Pythagorean theorem tells us that the size of the slip vector is 5: this is the hypotenuse of a right triangle with sides 3 and 4. But the P_AproxDistance function returns size 5.5. And here, we received acceleration for 10% simply because of an inaccuracy of calculations! The size of such a bonus depends on the angle of movement of the player, and unfortunately, if the angle is 45 degrees (the usual situation with the void glide trick), the bonus will be a measly 6%. And this is more than enough.
However, a little more effort is needed to achieve an amazing result. Usually P_SlideMove does this:
starts the meter starting from one
draws vectors from corners, and tries to move the player in the direction of one of them
if there is a remainder of the vector after a collision, it cheats a new vector directed parallel to the obstacle and tries to move the player in a new direction
if the movement "slip" is impossible, it starts all over again, increasing the counter by one
well, if the counter has already reached 3, helplessly gives up and calls for help “stairstep”.
Now the logic of what is happening has become obvious: if a player is in a corner, the first collision with the wall causes a slide to the second wall. A second collision occurs immediately there, and the algorithm is restarted, taking as the initial data the existing RTS - that is, the remainder of the “slip” from the previous cycle. The whole trick comes down to creating a situation where P_SlideMove constantly fails when trying to move. This is what happens:
three vectors known to us are drawn
the shorter of them intersect with the wall at a distance of less than 3.125% of its own length
instantly calculated "slip vector", using 100% of the current TSP
in the process of calculating, the PST grows due to the inaccuracy of the “approach” P_AproxDistance
however, slipping in a new direction is impossible; therefore, the process is restarted
described steps are repeated twice
the algorithm gives in, causing the stairstep, which also fails, but at the same time saves the player's twice-increased SPT.
But this is not all: if during a “run in the corner” a player’s SPT exceeds 15 units and is directed north or east, the described process will occur twice, thanks to splitting the movement into two parts. That is, an increase in PTA will occur four times in one tick!
Now we saw the whole process. You just need to choose the correct position and the code will enter the cycle, which gives a constant increase in TPS. Each SPT tick is increased twice (or four times) by a relatively small amount due to a rough approximation. At the end of each tick, an SPT grown up, of course, is reduced by "friction", but as long as the increase in TPS per tick is at least 10%, this is enough to exceed the decline due to friction and to ensure a constant increase. It remains to wait until SPT exceeds 32 units.
BLACK FRIDAY :30% discount on the first payment on the promo code BLACK30% when ordering for 1-6 months!
These are not just virtual servers! This is a VPS (KVM) with dedicated drives, which can be no worse than dedicated servers, and in most cases - better! We made VPS (KVM) with dedicated drives in the Netherlands and the USA (configurations from VPS (KVM) - E5-2650v4 (6 Cores) / 10GB DDR4 / 240GB SSD or 4TB HDD / 1Gbps 10TB available at a uniquely low price - from $ 29 / month , options are available with RAID1 and RAID10) , do not miss the chance to place an order for a new type of virtual server, where all resources belong to you, as on a dedicated one, and the price is much lower, with a much more productive hardware!