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.