Last server records
Pro Nub

Strafe Physics

Posted by Kpoluk 20 Aug 2016 in 07:16
We're going to understand strafes physics using open source code of Half-Life. Particularly, we need to analyse these files: input.cpp, pm_shared.c and pm_math.c.

Velocity is a vector. We can specify it either as three components (projections on the axis of the coordinate system) or as a pair – module of velocity (speed) and its direction (unit vector).
When fps is 100, CS engine processes mouse movements and button presses 100 times in a second. Then, using this data, engine calculates behavior of the player model. More precisely, engine doesn’t know which buttons are pressed, but gets commands like “go to the right”, “duck”, “jump” and so on, but we will talk about buttons for the sake of simplicity.

We will derive velocity of the player on each engine time in 5 steps:

  1. Get the state of movement buttons (WASD)

  2. On basis of retrieved states get the wishful velocity (the one player desires to have) in the coordinate system bound to the player.

  3. Knowing the direction which player is aiming at, find a relation (transition matrix) between coordinate system of the player model and coordinate system of the outer world (that is map).

  4. In case of friction presence (walking on the ground, for example) reduce the current speed.

  5. On basis of the matrix, retrieved on step 3, translate wishful velocity into the coordinate system bound to the outer world. Then add this velocity to the current velocity in a certain way. At that method of adding is different in cases of walking on the ground and flying in the air.

Step 1.

First of all, let’s look at this function from input.cpp:

As you can see, it returns val, which describes a state of some button. For instance, if the button was pressed at the previous frame and still pressed at the current frame, then val equals 1. And in case if the button wasn’t pressed neither at the previous frame nor at the current frame, then val equals 0.

Step 2.

Now in the same input.cpp examine a part of code, which from pressed buttons derives a wishful velocity (forwardmove, sidemove, upmove), i. e. a direction we want to move along at the current frame. At the end of the article it becomes clear what is the final direction of movement afterwards all calculations.

For obtaining wishful velocity components we use values of cl_upspeed (default is 320), cl_forwardspeed (400), cl_backspeed (400), cl_sidespeed (400). While holding Shift button we get the speed multiplied by 0.3 in Half-Life and by 0.52 in CS 1.6. Then if the speed turned to be more than maxspeed (250 with usp / knife), it’s scaled to be equal 250.

Step 3.

Let’s take up the part of PM_PlayerMove function from pm_shared.c:

What is that AngleVectors there? It’s kinda simple. Denote as
  • ICS – inertial coordinate system, which is bound to the outer world (map)
  • PCS – player coordinate system, which is bound to the player model

Wishful velocity that was retrieved in input.cpp, is written is PCS, at that PCS is a left-handed system (X axis points forward, Y right and Z up – these are the same directions which give us positive values of forwardmove, rightmove and upmove). We want to use wishful velocity for retrieving final velocity, which is written in ICS. So, AngleVectors function will provide us with a matrix of transition from ICS to PCS.

The input is the angles which determine a direction we looking along, i. e. position of PCS relative to ICS. These angles are called yaw (right-left, Z axis), pitch (up-down, Y axis) and roll (right-left tilt, X axis). For short denote them as

To retrieve a matrix of transition from ICS to PCS one need to multiply turning matrixes, corresponding to row-pitch-yaw angles. Plus one more matrix will set reflection of Y axis, so we will get a right-handed coordinate system from the left-handed one:

After multiplying we have exactly the same formulas as you could see in the code of AngleVectors. Required transition matrix looks like:

Here forward, right and up are unit vectors that specify PCS axis written in ICS.

Step 4.

Get back to PM_PlayerMove. Further we have PM_Friction function to be called (in case we are walking on the ground). The interesting part is:

For sv_friction 4 and 100 fps (meaning duration of each frame is 0.01 seconds) we have drop = control * 0.04. If our speed is more than sv_stopspeed (75 by default), than control equals to our speed, and drop comes to be 4 percent from it. These 4 percent is how much our speed will be slowed down at the output of PM_Friction.

Step 5.

After PM_Friction engine calls either PM_WalkMove (walking on the ground) or PM_AirMove (flying in the air). Let’s examine these cases separately.

Case 1: Walking

Here is PM_WalkMove:

We retrieve forwardmove and sidemove form input.cpp and then use forward and right from transition matrix for the translation of velocity into the coordinate system of the map, just as we planned.

In order to exclude the influence of movement buttons on vertical speed (in PCS) we don’t use upmove component and zero vertical components of forward and right (which forces us to normalize these vectors, so they won’t affect wishful speed). Then speed is clipped to maxspeed and PM_Accelerate is called, having direction (unit vector) of wishful velocity and sv_accelerate (5 on default) as parameters.

DotProduct is a scalar product of velocity and wishdir. It’s calculated by multiplying their components, but we are interested in a physical meaning. Since wishdir is a unit vector which determines a wishful direction, we actually project current velocity on the direction of wishdir. The more angle between them, the less projection is, i. e. currentspeed variable.

Next we retrieve addspeed as wishful speed minus currentspeed, and also accelspeed (accel * pmove->frametime * pmove->friction can be estimated as 5 * 0.01 * 1 = 0.05, so accelspeed is 5 percent form wishful speed).

Against correlation between addspeed and accelspeed one of these values become a length of speed gain, which is so desired by us. We add to the current velocity a vector, directed along wishdir and with accelspeed length (these vectors are both written in ICS, so this operation is absolutely legal).

Now let’s try to feel what it all means.

Experiment 1. You have pressed W button and now is running straight forward with knife or usp. PM_Friction at the current frame slows us down by 4 percent, so our speed is 240 units/s intead of 250. In that W is the only button pressed, input.cpp tells us that its state is 1, states of other movement buttons equal 0. We have cmd->forwardmove = 400, cmd->sidemove = 0, and after scaling (since we exceeded maximum speed of 250) cmd->forwardmove = 250, cmd->sidemove = 0. Vector (fmove, smove) in PM_WalkMove function reveals the direction we are looking along. Direction of wishdir is the same as direction of the current velocity, wishspeed is 250 and we are entering PM_Accelerate. Here
currentspeed = 240 * cos(0) = 240
addspeed = 250 - 240 = 10
accelspeed = 250 * 0.05 = 12.5 > 10

so accelspeed turns to be equal 10. Then we add 10 units/s to our speed of 240 (in the same direction) and speed now equals 250 units/s. That’s how we just walking straight forward.

Experiment 2. We are running the same way as in experiment 1 and suddenly pressing A. Our speed after PM_Friction is 240 units/s. W was pressed and is still pressed, so its state is 1. A is pressed just now, its state is 0.5 Thus, cmd-forwardmove = 400, cmd->sidemove = 200 and then after scaling cmd->forwardmove = 223.6, cmd->sidemove = 111.8. Inside PM_WalkMove after retrieving wishful velocity we pass into PM_Accelerate wishspeed = 250 and unit vector wishdir, which forms an angle of arctg(200 / 400) = 26.565 degrees with current velocity. In PM_Accelerate:
currentspeed = 240 * cos(26.565) = 214.66
addspeed = 250 - 214.66 = 35.34
accelspeed = 250 * 0.05 = 12.5 < 35.34

so accelspeed stays to be equal 12.5. We add 12.5 units/s to our 240 along the direction of wishdir. According to the cosine law:
x^2 = 240^2 + 12.5^2 + 2* 240 * 12.5 * cos(26.565) = 63122.77
x = 251.24

Thus, our velocity has turned to the left a bit and increased by 1.24 units/s.
During the subsequent frames the state of A button will be 1, therefore wishdir will form 45 degress with the direction of our view all the time. Here is the scheme of velocity adding:

As velocity vector is turning to the left, speed is changing in an intriguing way (cause of friction). It is decreasing for a few frames prior to 249, then growing up to 262 for 14 frames, and fluently falling for about 50 frames (recall that when fps is 100 every frame is 0.01 seconds long). Seems like we have explained a technique called fastrun.

In the end, velocity direction will be the same as wishdir. At that angle u between velocity and wishdir right after presing A is 26.565 degrees, than jumps up to 45 and gradually decreasing to zero. And here we have a curious question - what if we turn wishdir to the left all the time, keeping some constant u angle - how u would depend from velocity?

Experiment 3. Turning wishdir to the left means turning to the left direction of view. In order to do it with constant angular speed let's use «arrows» on keyboard (+left and +right commands). Just run with pressed A, W and left arrow. A few seconds later you will discover that you are running in a circle with a constant speed. In this PM_Friction will scale down the velocity every frame, while PM_Accelerate will turn it and increase by the same value.

Arrow rotation speed is defined by cvar cl_yawspeed, which is 210 by default. Let's reduce cl_yawspeed to 180. Radius of the circle we running in increased. Consequently u angle became less. In addition, addspeed is still more than accelspeed, so the length of added vector remains the same. And that's why established speed became bigger.

We continue diminishing cl_yawspeed. Everything's go well, about 118 we have assured 277 units/s, but at cl_yawspeed 117 speed began to drop. Thus we found out that at some angle u the growth of speed for one frame will be maximum. Let's draw a 3D plot with help of MATLAB that will show the dependence of the speed increase from the angle u and current speed:

In the third experiment we accelerated firstly i. e. were somewhere in red zone of the plot, than shifted to a constant angle u with zero speed increase i. e. fell into yellow zone. In this it's clear that the less we made u the more speed we were able to retain. However at speeds more than 250 units/s we meet the pit which does not allow us to gain speed at small values of u. There's no point to consider the plot beyond 277-278 units/s since none of possible angles u will give us speed gain.

Case 2: Flying

Well, now leave a ground alone. We will examine what happens in the air.
PM_AirMove is different from PM_WalkMove only in that PM_AirAccelerate is called instead of PM_Accelerate and the parameter passed is sv_airaccelerate (10 on default) instead of sv_accelerate. PM_AirAccelerate by itself is pretty the same as PM_Accelerate:

But here are two important differences in physics. Firstly, there is no friction in the air, secondly we cut wishspeed to 30 (wishspd variable is equal to wishspeed). Thus, we cannot use the same tactics as on the ground. Indeed, our currentspeed shouldn’t be more than 30, otherwise we won’t get any gain. And now we do the trick - refusing to use W and pressing A or D only. Also we should control angle u between current speed and wishful direction such that DotProduct won’t be more than 30.

However, let’s concentrate on numbers at first.

Take a run by pressing W, then jump, release W and press A. There is no friction affecting us since this moment, so at the first frame current velocity vector has length of 250 units/s and is oriented along the direction of our look. We will enter PM_AirAccelerate having wishdir to be directed to the left and wishspeed equal 250.
wishspd = 30
currentspeed = 0 (cos(90) = 0)
addspeed = 30 - 0 = 30 > 0
accelspeed = 10 * 250 * 0.01 * 1 = 25 < 30

so accelspeed remains to be equal 25. Final speed will be deviated to the left a bit, and according to the Pythagorean Theorem:
x^2 = 25^2 + 250^2 = 63125
x = 251.25

Thus, our speed is a bit more and we will fly large distance. Although, in fact the distance that we fly forward is absolutely the same as if we wouldn’t press any button after jump at all – total distance is more due to a small deviation to the left.

Just now the angle u between wishdir and the current speed was about 90 degress. Suppose that we are doing lj and our speed before jump is 275 units/s. Just as on the ground we're going to learn how speed gain depends on angle u.

Let’s try to find an angle u such that DotProduct would be equal 30. From cos(a) = 30 / 275 we retrieve u = 83.74 degrees. If u is less, speed is not changing, and if u is more we have a speed gain. Firstly gain is not that big, but with increasing of angle u the value of DotProduct will reach value of 5 (it will happen at the angle of acrcos(5 / 275) = 88.96 degrees), addspeed will be equal to 25 and subsequent growth of the angle u won’t affect the length of added vector – accelspeed will be equal to 25 too all the time. Moreover, this condition gives a maximum gain because further angle increasing will reduce the total velocity on account of vector addition. When the angle is more than 90 degrees, DotProduct is negative, addspeed is guaranteed to be more than accelspeed and we still have speed gain. In that vector addition schematically looks like:

At the angle of 92.61 degrees velocity will just turn, but its length will stay the same. At the angles u more than 92.61 we will loose the speed, at that the more angle, the more lost.

In total, we have three cases:

  1. u <= 83.74
    Velocity vector is not changing (neither module nor direction). If during the flight you turn your mouse to the left and press D, whereupon continue to drag mouse to the left, then your velocity will stay absolutely the same. Also if you are strafing with pressed W button, your velocity will stay the same as well.

  2. 83.74 < u < 92.61
    Your velocity vector is changing the direction towards strafe button and increase in length. Maximum of gain will take place at the angle of 88.96 degrees. You can see this gain in a Gain column of lj statistics.

  3. u >= 92.61
    Velocity vector is changing its direction and decreasing its length (or keeps same length for u = 92.61). This speed loss can be seen in a Loss column of lj statistics.

Turns out that in order to gain speed we have to keep angle u in a quite narrow gap. If your speed is 340 units/s (very good result for lj) than the gap diminishes to 84.94 < u < 92.11, at that maximum of gain corresponds to u = 89.16. Besides the gain itself will become less - at the beginning of jump you can add more than 1.5 units/s and at the end no more than 1.3 units/s. Summing up, the more speed, the harder to make gain.

Now let set a goal to maintain angle u which will give us maximum speed gain (we will call it optimal). We need to understand how fast we should move the mouse. Assume that x is an angle by which velocity will trun during one frame, and v is a current speed. From law of sines we get:

sin(x) / 25 = sin(180 - x - (180 - u)) / v
v * sin(x) = 25 * (sin(u) * cos(x) - cos(u) * sin(x))
tg(x) = sin(u) / (v / 25 + cos(u))

At v = 275, u = 88.96 we retrieve x = 5.186 degrees, and at v = 340, u = 89.16 we have x = 5.035 degrees.

It means that if we were able to keep some optimal u we would have to do our strafes smoother as speed increases. However as we can see this effect is so insignificant there's a point to recall it only at really big speeds. For a normal lj it's more important to understand how transition from ground prestrafe to air strafe works, and what happens during the switch between strafes.

We will talk about these things in the next article dedicated to LongJump physics. And for now we're going to examine 3D plot showing dependence of speed gain from angle u and current speed during the flight:

There is no speed gain at yellow plateau, speed loss at blue pit and only at the knoll in the middle we can speed up. And the more speed the lower and narrower this knoll becomes.

That's all for now, we will continue to deal with lj in the next article.