The algorithm behind the steering assistant in the Superior Software release
The 1986 Superior Software release of Revs introduced a brand new feature: computer assisted steering, or CAS for short. No doubt inspired by the fact that the original Acornsoft version of Revs is fiercely difficult to master - particularly on keyboard - this new feature helps drivers navigate around the track, but it isn't an autopilot in the traditional sense; it doesn't grab the wheel, and it doesn't wrestle any control from the player.
Instead, CAS is a more subtle experience that somehow guides you around the trickier corners without appearing to get in the way at all. It really does feel like a gentle helping hand on the steering wheel, and it makes life a lot easier, but without spoiling the challenge.
The keyboard control document that comes with the game has this to say about CAS:
CAS does not alter the characteristics of the simulator, but is merely a "hand" on the steering wheel. So it's still up to you to get your speed and position correct as you approach each bend.CAS provides steering demands to the simulator in response to your key or joystick movements and fills in the detail that you cannot provide very easily except with an analogue joystick. It does this by taking into account the severity of oncoming bends and the position of the car relative to the side of the track and basically tries to get round the bend without coming off the track.
So what does CAS do, and how does it do it? Let's take a look.
CAS is a light touch
CAS helps us drive by gently nudging the steering wheel in the correct direction - it doesn't take over, it just helps us out a bit. It only gets applied if it is switched on, and if we are already steering using the joystick or holding down a steering key on the keyboard (so CAS doesn't take control away from us - we have to be steering already for CAS to have any effect). It also only works if we are driving forwards along the track.
The way that CAS helps us steer is really clever, though it does stand on the shoulders of giants - specifically, it uses the segment steering values that we calculate when implementing steering in the non-player drivers. This is described at length in the deep dive on tactics of the non-player drivers, but for the purposes of this article, we can assume that every segment in the track segment buffer has an associated segmentSteering value - so that's every track segment in our field of view.
The value of segmentSteering for a segment in the track segment buffer contains the amount and direction of steering that should be applied in order to follow the optimum racing line around the track. This is the line that the non-player drivers strive to follow, only deviating for overtaking manoeuvres. This value is therefore a perfect basis for a system that is supposed to help us around the track; we already have the ideal steering amount stored in the segmentSteering table for the track ahead of us, so CAS simply takes this value and nudges the steering wheel towards the optimum steering amount.
The format of each value in the segmentSteering table is as follows:
- Bit 7 is the steering direction (0 for steering left, 1 for steering right)
- Bit 6 determines how this steering value should be applied to the driver (0 means this steering is always applied to the car, 1 means it is only applied if there is enough room on the track)
- Bits 0 to 5 contain the amount of steering, expressed in terms of the change in racing line. The range in racing line is 0 (full right) to 255 (full left), so steering by +26 would steer the car to the left by 10% of the track width, for example
Before we look at the algorithm, we should also note the format of the (steeringHi steeringLo) variable, which contains the position of the steering wheel (i.e. the dash that we move around the edges of the wheel when steering):
- Bit 0 of steeringLo is the steering direction (0 for steering right, 1 for steering left)
- Bits 1 to 15 contain the amount of steering
Note that the steering wheel direction in bit 0 is the other way round to the direction in bit 7 of segmentSteering.
Given this, let's see how the CAS algorithm works.
The CAS algorithm
In each case, the routine is only called if CAS is enabled, which is detected by the GetSteeringAssist routine. This routine returns the configuration status, and it also draws a caret symbol on the rev counter if CAS is enabled, using the configuration flag to generate the pixel bytes required (a very efficient solution). The new configuration keys for CAS (SHIFT-f6 to enable, SHIFT-f3 to disable) are implemented by adding two extra values to the configKeys table and a new config variable at configAssist, which are automatically supported by the ProcessShiftedKeys routine.
If CAS is enabled, then AssistSteering is called with (U T) set to the current amount of steering from the keyboard or joystick. We only use the current steering value to determine the direction in which we are trying to steer; for keyboard users, this makes sense as steering is digital (left or right), but for joystick users this means that the steering acts like a digital joystick when CAS is enabled, as we throw away any analogue joystick value at this point (this is pointed out on the keyboard control document that comes with the game, where it says "analogue joysticks will steer as digital sticks when CAS is on", and this is why).
The call to AssistSteering is made just before (U T) gets applied to the position of the steering wheel, so the CAS algorithm simply replaces this amount with a value that includes an extra bit of help to steer us around the track, based on the optimum steering given in segmentSteering.
Here's how it works. First, we set the following if we are using the controls to steer right:
X = 10
or the following if we are using the controls to steer left:
X = 50 = 10 + 40
This sets X to the index of a segment in the track verge buffer (see the deep dive on data structures for the track calculations for details of the verge buffer). Specifically, it is the index of the segment at offset 10 in the verge buffer, which is the fifth element in the track segment list (as entries 0 to 5 are the track section list, with the track segment list starting at index 6). If we are steering to the right, then the index is 10, which points into the verge buffer for the inner verge, but if we are steering left, then we add 40 to the index to take us from the inner verge buffer to the outer verge buffer.
Or, to put it more simply, X is the index in the verge buffer of the segment that's five segments towards us from the furthest visible segment, on the side of the track that we are trying to steer towards. We use this below to temper the assistance from CAS so it doesn't steer us off the track.
Next, we set the following if we are using the controls to steer right:
(W V) = (steeringHi steeringLo) + 256
or the following if we are steering using the controls to steer left:
(W V) = (steeringHi steeringLo) - 256
This gives us an adjusted steering wheel position, starting from the current position of the steering wheel (i.e. the line on the steering wheel rim). This adjusted position is in the direction that we are steering, so if we are steering right, this value is one "tick" to the right from the current steering wheel position.
Next, we calculate a variable U that will contain the amount of steering assistance we're going to give. We start with the following:
U = 32 + max(0, 60 - playerSpeedHi) * 2
so U is a value in the range 32 to 152, with a higher value at lower speeds.
We now check bit 6 of segmentSteering. If this is clear, then this means the steering in segmentSteering would be applied to any non-player drivers in this segment, so we incorporate this into our calculation (where |segmentSteering| is segmentSteering with bit 7 cleared, to remove the direction and extract the steering magnitude):
U = max(U, 16 * min(|segmentSteering|, 7))
The value of 16 * min(|segmentSteering|, 7) is in the range 0 to 112, so this potentially makes the value of U higher when we have sharper steering in segmentSteering. This means there is more chance of CAS raising our original value of U when we are at higher speeds (when the original value of U will be lower) or have higher values of segmentSteering (for sharper bends). The impact of the segmentSteering element is restricted to a maximum of 7, so even on sharp corners, the impact of the assistance is gentle due to the min() function.
U represents the amount of steering assistance we're going to give, so we now calculate the value of (U T) that we need to return to the ProcessDrivingKeys routine to implement this steering. The calculation is as follows:
(U T) = U * (X-th xVergeRight - (W V)) / 256
The X-th xVergeRight is fetched from the verge buffer. This is the yaw angle (i.e. the x-coordinate) of the verge that we mentioned above - the fifth track segment counting towards us from the start of the verge buffer, on the side that we are trying to steer towards. Subtracting (W V) from this gives us a value that represents the amount we would have to turn the steering wheel in order to steer towards that part of the verge. We then multiply this distance by U / 256, which scales this amount of steering to a value that is proportional to the segment steering value, so CAS not only assists us to follow the optimum steering for this segment, it has less effect as we steer closer to the verge, so as not to push us off the track.
Finally, we set bit 0 of (U T) to the direction of steeringLo, so CAS assists us in the same direction as the existing steering (so to get good assistance, you need to be steering in the correct direction).
Making room for CAS
Revs does not have a lot of spare memory (see the deep dive on the Revs memory map for details), so something had to give before CAS could be added. This takes the form of the following parts of the code, which between them freed up enough space to squeeze in the AssistSteering routine, plus the configuration code in
- SetupGame: A clever solution for freeing up memory is to move the SetupGame routine, which is run when the game loads, but is never needed again. So in the Superior version, SetupGame is put into the same block of memory as the carSpeedLo, totalPointsLo and totalPointsLo variables, which are only used after the game has started.
- GetColourSup: This long routine was rewritten to be much shorter, and much easier to understand. Compare the one-part Superior Software version to the original, which came in three parts: part 1, part 2 and part 3.
In all, these changes allowed enough room for AssistSteering to slip in, along with other driving-related fixes in SetSteeringLimit and SetPlayerDriftSup, and the configuration changes in GetSteeringAssist. Space might have been tight, but CAS is such a useful feature that it was definitely worth the effort.