In this chapter we will discuss a few algorithms or implementations of movement
automation in a typical TAS tool.
10.1. Vectorial compensation
In Strafing, we discussed various ways to βstrafeβ with differing
objectives, most commonly maximum-acceleration strafing and speed-preserving
strafing. In each of these methods, we must derive a formula or, at least, an
implicit equation for the angle π between π― and
Λπ to realise the desired strafing objective.
Unfortunately, Half-Life does not allow us to realise the angles to a very high
precision. This is because Half-life uses the DELTA mechanism (see DELTA)
to transmit information across the network from client to server and vice versa.
Looking at the default delta.lst included in vanilla Half-Life, we see the
following line defined under usercmd_t:
DEFINE_DELTA( viewangles[1], DT_ANGLE, 16, 1.0 ),
This shows that the yaw angle is somehow lossily compressed to 16 bits. This
means that, to transmit some angle π in degrees, it is first converted
at the client side to the 16-bit integer representing
intβ‘(65536360π)=intβ‘(ππ’π)
where intβ‘(π₯) is the integer part of some real number
π₯. When the server receives this integer, it is then converted back to
the floating point number
36065536(intβ‘(65536360π)π°π½π³65535)
This expression looks familiar! Indeed, this entire operation is equivalent to
the anglemod function, first described in Anglemod. Given our
understanding of anglemod, we know that there are only exactly 65536 possible
angles. Although this is already very precise, in the sense that any angle can
be realised with only about 0.0015% absolute error, it is possible to do better
by controlling the yaw angle π in combination with πΉ
and π.
By controlling πΉ and π, we can change the direction of the unit
acceleration vector Λπ (see Air and ground movements)
without changing the yaw angle. To see why, recall that π =πΉΛπ +πΛπ¬, and even keeping
Λπ and Λπ¬ constant, we can change
the βweightsβ πΉ and π to make the vector sum point to any
desired direction, and therefore realise any angle π between
π― and Λπ. Unfortunately, again, looking
at the file delta.lst, we see the following lines:
DEFINE_DELTA( forwardmove, DT_SIGNED | DT_FLOAT, 12, 1.0 ),
...
DEFINE_DELTA( sidemove, DT_SIGNED | DT_FLOAT, 12, 1.0 ),
DEFINE_DELTA( upmove, DT_SIGNED | DT_FLOAT, 12, 1.0 ),
Here, the 12 indicates that the client side ΛπΉ,
Λπ, and Λπ (see Forwardmove, sidemove, and upmove for their definitions)
will be truncated to 12 bits and integer precision. Since 212 =4096,
and a signed integer transmitted across the network is not represented in
twoβs complement, each of these values will be clamped to [ β2047,2047].
Nevertheless, despite each of π, πΉ, and π being
truncated, we can still combine them to obtain a more accurate angle realisation
method than possible using only the yaw or the FSU.
10.1.1. Unconstrained compensation
Suppose we have computed the desired angle π between the velocity
π― and the ideal unit acceleration vector
Λπ. To βrealiseβ this angle is to strafe such that the
ideal unit acceleration Λπ is attained by changing the
π, πΉ, and π. However, due to the slight
imprecision in each of these values, the ideal Λπ can
never be attained exactly. Instead, we compute the βbestβ approximation to it,
which may be written as
ΛΛπ=Λππ
(π)
where Λπ is the two dimensional unit forward vector (see
View vectors), and π
(π) is the rotation matrix. We may then
define the βbestβ approximation such that the angle between
Λπ and ΛΛπ is minimised. In
the ideal case, the angle between the two vectors is zero, meaning the desired
unit acceleration vector is attained exactly. However, in practice, there will
be a small difference in angle π such that
Ξ =Λπβ
ΛΛπ=cosβ‘π
Writing out the dot product and simplifying, we obtain
Ξ =ππ₯cosβ‘(π+π)+ππ¦sinβ‘(π+π)
where
Now, to minimise the angle π is to maximise cosβ‘π,
and therefore maximising Ξ . We may compute the maximum point of
Ξ by solving
πΞ π(π+π)=βππ₯sinβ‘(π+π)+ππ¦cosβ‘(π+π)=0
yielding
tanβ‘(π+π)=ππ¦ππ₯
Note that π =π’π for some integer π β[0,65535].
Inverting the tangent function and computing the remainder when divided by
π’ (i.e. modulo π’),
πββππ’βπ’=atan2β‘(ππ¦,ππ₯)ββ1π’atan2β‘(ππ¦,ππ₯)βπ’
Notice that the yaw π is gone, and so is the integer yaw
π. This is because the yaw is always divisible by π’, and
therefore its remainder is always zero. To slightly simplify implementation,
write
ππ’ββππ’β=1π’atan2β‘(ππ¦,ππ₯)ββ1π’atan2β‘(ππ¦,ππ₯)β
That is, if this equality is satisfied, then the π (and an appropriate
π) will maximise Ξ , achieving the best approximation.
In practice, however, this equality is rarely satisfied, due to the
imprecision in π and πΉ mentioned previously. Denote
(10.1)ΛΞ¦=ππ’ββππ’βΞ¦=1π’atan2β‘(ππ¦,ππ₯)ββ1π’atan2β‘(ππ¦,ππ₯)β
Then, we want to find a ΛΞ¦ that is the closest to
Ξ¦, subject to the constraints that π and πΉ have. One
way to do this is to brute force every possible combinations of π and
πΉ and computing the corresponding ΛΞ¦ values. However,
this is very inefficient and can take millions of iterations. Doing it once on a
fast computer may not consume a noticeable amount of time, but when implemented
in game, these computations need to be done every frame, and there could be
thousands of frames per second.
A better approach is to build the vectorial compensation table (VCT). The
details in how to compute such a table will be described in VCT generation. But here, we will assume that it contains 3-tuples
(ΛΞ¦,πΉ,π), sorted by ΛΞ¦, where
ΛΞ¦ is computed using (10.1) using the
corresponding π and πΉ. To find the closest ΛΞ¦
to Ξ¦, we may use binary search to find entries corresponding to
ΛΞ¦1 and ΛΞ¦2 such that
Then, the value that is closest to Ξ¦ would simply be one of
ΛΞ¦1 and ΛΞ¦2. This algorithm is very fast
because even if the VCT contains millions of entries, it takes at most about 20
iterations to find the two ΛΞ¦ entries. The downside is that
there will be a small but noticeable delay in generating the VCT.
As a side note, an alternative way to compute Ξ¦ exists. In practice,
computing atan2β‘(ππ¦,ππ₯) may be slightly less efficient,
because obtaining Λπ requires computing the rotation
matrix π
(π), which in turn requires computing sin and
cos along with multiple addition and multiplication operations. An
alternative method involves observing that ππ₯ =cosβ‘(π½ +π)
and ππ¦ =sinβ‘(π½ +π), where
π½=atan2β‘(π£π¦,π£π₯)
Therefore,
atan2β‘(ππ¦,ππ₯)=π½+π+ππ
for some integer π. This implies that
Ξ¦=π½+π+πππ’ββπ½+π+πππ’β=π½+ππ’ββπ½+ππ’β
as π’ divides ππ, and so the integer disappears, simplifying
the expression. This method of computing Ξ¦ requires only one
trigonometric computation, namely in obtaining π½.
Footnotes
10.1.2. VCT generation
The algorithm described in previous sections rely on a lookup table called the
vectorial compensation table (VCT). As a reminder, this is a table containing
entries of 3-tuple (ΛΞ¦,πΉ,π) sorted by ΛΞ¦,
and that each entry must have a unique π and must satisfy πΉ2 +π2 β₯π2π.
If we simply enumerate πΉ and π by drawing each element from
[ β2047,2047], then the resulting ΛΞ¦ will not be
unique. So see why, suppose ππ =320, (πΉ1,π1) =(400,800),
and (πΉ2,π2) =(800,1600). Then, obviously πΉ21 +π21 >π2π and πΉ22 +π22 >π2π, but π1 =π2 and
therefore ΛΞ¦1 =ΛΞ¦2.
To obtain a set of unique ΛΞ¦, we must therefore enumerate all
unique coprime pairs (πΉ,π), but satisfying the constraint that
β2047 β€πΉ,π β€2047 and πΉ2 +π2 β₯π2π. The most
efficient way to enumerate this is by generating a Farey sequence. To reduce the
generation time, we can exploit symmetries in (πΉ,π).
Firstly, we can restrict generation in just one quadrant, namely by considering
only positive πΉ and π. This is because for all πΉ and
π, we know that
π=atan2β‘(π,πΉ)=atan2β‘(|π|,|πΉ|)+π2π
for some integer π. Computing the ΛΞ¦,
ΛΞ¦=ππ’ββππ’β=atan2β‘(|π|,|πΉ|)π’+16384πββatan2β‘(|π|,|πΉ|)π’+16384πβ
Obviously 16384π ββ€, therefore this simplifies to
atan2β‘(|π|,|πΉ|)π’ββatan2β‘(|π|,|πΉ|)π’β=atan2β‘(π,πΉ)π’ββatan2β‘(π,πΉ)π’β
This implies that the ΛΞ¦s computed from (πΉ,π) and
(|πΉ|,|π|) are the same.
Within a quadrant, we only need to consider π in [0,π/4).
In other words, we only need to generate values within an octant. To see this,
define sets
π={ΛΞ¦(π)β£0β€π<π4}π={ΛΞ¦(π)β£π4β€π<π2}
Then we claim that π =π, and therefore only one of them needs to be
computed. Consider π β[0,π/4) in the domain of π. Then
clearly π +π/4 β[π/4,π/2), which is the domain of
π. Compute the corresponding ΛΞ¦, we have
ππ’+8192ββππ’+8192β=ππ’ββππ’β
This implies that π βπ. Similarly, consider πβ² β[π/4,π/2) in the domain of π, then πβ² βπ/4 β[0,π/4) is in the domain of π. It can be similarly shown that the
ΛΞ¦s computed using πβ² and πβ² βπ/4 are
the same. This shows π βπ, and we conclude that π =π.
One observation may be made regarding the relationship between different
elements in the π-table. Define sets
π={ΛΞ¦(π)β£0β€π<π8}π={ΛΞ¦(π)β£π8β€π<π4}
Consider some 0 β€π <π/8. Then, we have
tanβ‘(π4βarctanβ‘ππΉ)=1βπ/πΉ1+π/πΉ=πΉβππΉ+π
Computing arctan,
π4βπ=π4βarctanβ‘ππΉ=arctanβ‘πΉβππΉ+π=arctanβ‘πβ²πΉβ²=πβ²
Let π such that ΛΞ¦(π) βπ. Let integers
ππ =ππβ² and ππ =πΉβ² for some π, where gcd(π,π) =1. Namely, π/π is the completely reduced fraction of πβ²/πΉβ².
Then if π β€2047 is satisfied, we have ΛΞ¦(πβ²) βπ. In addition, we also have
ΛΞ¦(πβ²)=πβ²π’ββπβ²π’β=βππ’ββππ’
Namely, ΛΞ¦(π) +ΛΞ¦(πβ²) =1 if π/π’ is
not an integer.