10. Movement automationΒΆ

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

πœ‰=atan2⁑(𝑆,𝐹)

Now, to minimise the angle πœ– is to maximise cosβ‘πœ–, and therefore maximising Ξ . We may compute the maximum point of Ξ  [1] 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 [2]

˜Φ1β‰€Ξ¦β‰€ΛœΞ¦2

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.

10.2. Line strafingΒΆ

10.3. Automatic actionsΒΆ