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:

- Get the state of movement buttons (WASD)

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

- 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).

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

- 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:

`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.

`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.

`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.

HaMMaLast thing do you do ur lj in straight line or turn a bit (turn left if lj started with left pre).

thnx