Lobsters retrocomputing - 03 Sep 2026

Page 3 of 5

The final piece of the duplication puzzle is the stardust, which is drawn at the end of the main flight loop, after all of the ship slots have been iterated through and drawn. The STARS routine is responsible for drawing stardust, as described in the deep dives stardust in the front view and stardust in the side views. Stardust coordinates are stored in six different heaps, with one byte in each heap for each particle of stardust, storing the 16-bit x-coordinate in (SXL SX), the 16-bit y-coordinate in (SYL SY) and the 16-bit z-coordinate in (SZL SZ). Because the two space views in two-player Elite are half the size of the space view in single-player Elite, we can split the existing stardust particles between the two views, with player 1's stardust coordinates in the first half of each heap, and player 2's stardust in the second half.

The only fiddly bit is that stardust moves according to the player's movement, and in particular the alpha (roll), beta (pitch) and delta (speed) values, and it would be a bit of a pain to recode all the star-moving routines to cater for the different values for player 1 and player 2. So instead we use three new routines to apply another slightly hacky workaround:

- SaveShipMovement saves player 1's movement variables into a cache.

- GetPlayer2Movement copies player 2's movement data into the various ALPHA, BETA and DELTA variables that are used in the stardust calculations.

- LoadShipMovement restores player 1's movement variables from the cache.

We can then insert a shim into the start of the STARS routine to do the following:

- Call SaveShipMovement to store player 1's movement data.

- Call GetPlayer2Movement to fetch player 2's movement data.

- Move and draw player 2's stardust in player 2's view, using the second half of each stardust heap.

- Call LoadShipMovement to revert to player 1's movement variables.

- Move and draw player 1's stardust in player 1's view, using the first half of each stardust heap.

And that's how we draw player 2's view... except we haven't talked about the maths behind all of this, and that's the most important part of all, so let's do that now.

The geometry behind player 2's view

-----------------------------------

As explained above, the local bubble in two-player Elite is essentially a single-player bubble, just like the original game, with player 1 at the centre of the universe. Player 2 is just another ship in the bubble, in slot #2, and when we draw player 1's space view, we effectively use the same code as in the original game, we just crop it to the top half of the screen.

The challenge is to draw the same ships, but in player 2's space view and from player 2's perspective. The previous section explains how we duplicate the current ship into a higher-numbered slot, where we transform the ship's coordinates and orientation into player 2's perspective, and then we draw the results in the bottom space view.

In this section we look at that transformation process, which is the most important part of the entire hack. In order to follow along, you'll probably want to read the deep dive on orientation vectors, as we're going to be working with them a lot. Also, the core mathematical concepts are similar to those discussed in the deep dives on back-face culling and calculating vertex coordinates, particularly the bit about "scalar projection" in the first article and "transposing the rotation matrix" in the second, so you might find those useful too. I'll try to explain things as I go along, but sometimes it helps to have more than one explanation of a concept to hand.

Now that I've failed to put you off, let's try a thought experiment. Imagine you are playing two-player Elite in virtual reality, and you are currently sitting in player 1's ship. Out there in the distance you can just about see player 2's ship, and also in the same local bubble of universe are the planet, the sun and a missile or two. And imagine you can move your point of view to anywhere in this universe by pinching, grabbing and rotating, or whatever it is that the cool VR kids do these days.

The question is this: starting out with us sitting in player 1's cockpit, what do we need to do in order to see the view from player 2's cockpit? In other words, rather than moving ourselves, how do we grab and rotate the virtual universe around us in order to get to player 2's view? If we can answer this, then that's what we need to build into two-player Elite to let us calculate what the bubble looks like from player 2's point of view.

Intuitively, this is what I would do to answer this question. I'd drag the universe whole towards me, pulling player 2 closer and closer until my view was inside their cockpit, and then I'd rotate the whole lot around me until I was looking out of the ship's front view. This dragging and rotating process doesn't only move player 2's ship towards us and into the right position, but it also moves and rotates everything else in the bubble, including our original ship, i.e. player 1's ship.

In other words, we have just worked out a geometric transformation that we can apply to each ship in the bubble that will move that ship into the correct position and orientation for the view from player 2's ship. The first part (the dragging) is known as a "translation", and the second part is a rotation, so we now have a two-part translate-and-rotate transformation that takes ships from the coordinates and orientation they have when we are looking out of player 1's ship, and moves them to the coordinates and orientation they have when we are looking out of player 2's ship.

The next step is to convert this translate-and-rotate transformation into a mathematical process. We can then apply that process to the current ship in the main flight loop, and can draw the result in player 2's view. As a reminder, the transformation process is this:

- Apply a translation that moves player 2's ship to our position (so this is us dragging to the position of player 2's ship to our original position in player 1's ship).

- Apply a rotation that takes our current view direction (which was down the nose of player 1's ship) and spins the view around us until we are looking down the nose of player 2's ship.

We need to apply this two-part transformation to both bits of data that define the position and orientation of the current ship, so we need to apply it to the current ship's coordinates in space, to move the ship to the correct position relative to player 2's view, and we need to apply it to the ship's orientation vectors, to rotate the ship to the correct orientation relative to player 2's view. In the latter case, applying a translation to an orientation vector doesn't affect the orientation, so we only need to do the second step when transforming the current ship's orientation.

Now that we know what we need to do, let's look at how we can implement this transformation mathematically. We'll look at implementing the problem in two different ways: first, by considering Elite's orientation vectors, and second by looking at rotation matrices. These two approaches are mathematically the same, they just use different terminology to explain the same algorithm, so hopefully at least one of them will help clarify this relatively complicated process.

In terms of orientation vectors

-------------------------------

The first operation is reasonably simple. We start out with player 1 at the origin (0, 0, 0), which is at the centre of the universe, and with player 2's ship at the coordinates defined in the ship data block for slot #2 - let's call those coordinates (x2, y2, z2). We therefore need to apply a translation that moves player 2's ship from (x2, y2, z2) to player 1's ship at (0, 0, 0). This is easy enough; we need to apply a translation of (-x2, -y2, -x2).

Another way of thinking of this translation is that when we drag the universe towards us in the first part of our virtual reality thought experiment, we pull everything along the vector that joins player 1 and player 2, and we pull it all in the direction from player 2 to player 1. The vector from player 1 to player 2 is [ x2 y2 z2 ], so this means the vector from player 2 to player 1 is the reverse of that, which is [ -x2 -y2 -x2 ].

So this is our translation step, which we apply to the current ship. If the current ship's coordinates are at (x, y, z), then this is the translation:

[ x ] [ x ] [ x2 ]

[ y ] -> [ y ] - [ y2 ]

[ z ] [ z ] [ z2 ]

The second operation in our transformation is the rotation, which is a bit more complicated. We need to apply a rotation that starts with us looking along player 1's viewing direction and spins things around until we are looking along player 2's viewing direction.

This is where the orientation vectors come in. Player 2's orientation vectors describe the direction in which player 2 is pointing relative to player 1, with nosev pointing out of player 2's nose, roofv pointing out of player 2's roof, and sidev pointing out of player 2's right side. These orientation vectors are orthonormal, so they are all unit vectors of length one, and this means we can use the same "scalar projection" approach as we do for back-face culling (see the deep dive on back-face culling for details).

As a reminder, scalar projection is the following property: given a vector and a unit vector, we can calculate the projection of the vector onto the unit vector by simply calculating the dot product of the two vectors. If we do this with a vector and three unit vectors, then the dot product gives us that vector, but expressed in terms of the three unit vectors - in other words, this is how we convert coordinates from one set of axes to another. This is the same as converting a vector from one perspective to another, or one frame of reference to another, which is what we need to do when drawing player 2's view. It's also worth remembering that coordinates and vectors are effectively the same thing; a coordinate is simply a vector with one end at the origin.

In this case, then, we want to take the following four vectors, which between them define the position and orientation of the current ship, just after we have applied the first step of our transformation:

- The updated coordinate of the current ship from above, as a vector

- The side orientation vector of the current ship

- The roof orientation vector of the current ship

- The nose orientation vector of the current ship

We want to apply the second step of our two-step transformation to each of these vectors by using scalar projection to project them onto player 2's orientation vectors, which moves them into player 2's frame of reference. So if [ x y z ] represents one of the vectors above, and player 2's orientation vectors are given by side2v, roof2v and nose2v, then we can apply the second step of our transformation by applying the dot product as follows:

x -> [ side2v_x side2v_y side2v_z ] . [ x y z ]

y -> [ roof2v_x roof2v_y roof2v_z ] . [ x y z ]

z -> [ nose2v_x nose2v_y nose2v_z ] . [ x y z ]

We apply the dot products in this order because when we are sitting in a ship, the x-axis points out of the right side of the ship, the y-axis points up and out of the roof of the ship, and the z-axis points forwards and out of the nose of the ship. So projecting the [ x y z ] vector onto each of player 2's orientation vectors will project the vector onto the axes that we use when we are sitting inside player 2's ship. And that is what changes the perspective of each vector to that of player 2's pilot, which is what we want in order to draw player 2's view.

If we combine both steps of the transformation - i.e. the translation and the rotation - then we get the following result when we apply it to the current ship's coordinate in (x, y, z):

x -> [ side2v_x side2v_y side2v_z ] . ( [ x y z ] - [ x2 y2 z2 ] )

y -> [ roof2v_x roof2v_y roof2v_z ] . ( [ x y z ] - [ x2 y2 z2 ] )

z -> [ nose2v_x nose2v_y nose2v_z ] . ( [ x y z ] - [ x2 y2 z2 ] )

And we get the following result when we apply the transformation to the orientation vectors for the current ship, because the translation step can be dropped (as it doesn't affect orientation):

sidev_x -> [ side2v_x side2v_y side2v_z ] . [ sidev_x sidev_y sidev_z ]

sidev_y -> [ roof2v_x roof2v_y roof2v_z ] . [ sidev_x sidev_y sidev_z ]

sidev_z -> [ nose2v_x nose2v_y nose2v_z ] . [ sidev_x sidev_y sidev_z ]

roofv_x -> [ side2v_x side2v_y side2v_z ] . [ roofv_x roofv_y roofv_z ]

roofv_y -> [ roof2v_x roof2v_y roof2v_z ] . [ roofv_x roofv_y roofv_z ]

roofv_z -> [ nose2v_x nose2v_y nose2v_z ] . [ roofv_x roofv_y roofv_z ]

nosev_x -> [ side2v_x side2v_y side2v_z ] . [ nosev_x nosev_y nosev_z ]

nosev_y -> [ roof2v_x roof2v_y roof2v_z ] . [ nosev_x nosev_y nosev_z ]

nosev_z -> [ nose2v_x nose2v_y nose2v_z ] . [ nosev_x nosev_y nosev_z ]

These are the calculations that are implemented in part 3 of DrawPlayer2View, with the latter calculation being done in the OrientateMissile routine. They enable us to take an arbitrary ship's position and orientation within player 1's space view, and they give us the position and orientation of that ship within player 2's view. This, therefore, is the heart of two-player Elite.

There is one more part to the story, because we can simplify this calculation considerably when transforming player 2's ship. As mentioned above, slot #2 contains player 2's ship, but if we took player 2's ship and transformed it into player 2's view, then it would simply move to the origin and we wouldn't need to draw it. This is intuitive, but it also falls out of the maths fairly easily: if we applied the translation step to player 2's ship at (x2, y2, z2), then the result would be (0, 0, 0).

So instead of transforming player 2's ship into player 2's view, we instead we repurpose this ship slot (which we duplicate into slot #12) for drawing player 1's ship in player 2's view.

We can work out where player 1's ship ends up within player 2's view using the same process: by applying the above transformation to player 1's ship. Of course, player 1's ship doesn't have a slot, because in the game's bubble of universe, player 1's ship is always at the origin (i.e. the centre of the universe), and it always has the three axes as its orientation vectors (the z-axis is always pointing out of the nose of player 1's ship, for example). So when we work out player 1's position and orientation in player 2's view by applying the above transformation to player 1's coordinates and orientation vectors, we end up applying the transformation to the coordinates of player 1 at (0, 0, 0), and to the three axis unit vectors (as they match player 1's orientation).

We can therefore simplify the calculation quite a bit for this specific case. For the first calculation, we get the following simplification because the current ship's coordinate is (0, 0, 0):

x -> [ side2v_x side2v_y side2v_z ] . ( [ 0 0 0 ] - [ x2 y2 z2 ] )

y -> [ roof2v_x roof2v_y roof2v_z ] . ( [ 0 0 0 ] - [ x2 y2 z2 ] )

z -> [ nose2v_x nose2v_y nose2v_z ] . ( [ 0 0 0 ] - [ x2 y2 z2 ] )

which gives us:

x -> [ side2v_x side2v_y side2v_z ] . [ -x2 -y2 -z2 ]

y -> [ roof2v_x roof2v_y roof2v_z ] . [ -x2 -y2 -z2 ]

z -> [ nose2v_x nose2v_y nose2v_z ] . [ -x2 -y2 -z2 ]

And for the second calculation we get the following simplification, because the current ship's orientation vectors in sidev, roofv and nosev are the unit vectors:

sidev_x -> [ side2v_x side2v_y side2v_z ] . [ 1 0 0 ]

sidev_y -> [ roof2v_x roof2v_y roof2v_z ] . [ 1 0 0 ]

sidev_z -> [ nose2v_x nose2v_y nose2v_z ] . [ 1 0 0 ]

roofv_x -> [ side2v_x side2v_y side2v_z ] . [ 0 1 0 ]

roofv_y -> [ roof2v_x roof2v_y roof2v_z ] . [ 0 1 0 ]

roofv_z -> [ nose2v_x nose2v_y nose2v_z ] . [ 0 1 0 ]

nosev_x -> [ side2v_x side2v_y side2v_z ] . [ 0 0 1 ]

nosev_y -> [ roof2v_x roof2v_y roof2v_z ] . [ 0 0 1 ]

nosev_z -> [ nose2v_x nose2v_y nose2v_z ] . [ 0 0 1 ]

which gives us:

sidev_x -> side2v_x

sidev_y -> roof2v_x

sidev_z -> nose2v_x

roofv_x -> side2v_y

roofv_y -> roof2v_y

roofv_z -> nose2v_y

nosev_x -> side2v_z

nosev_y -> roof2v_z

nosev_z -> nose2v_z

So when we are processing slot #2, we can apply the exact same transformation process to player 1's ship by using these simplified calculations, which saves us a fair bit of time when calculating the position and orientation of player 1's ship within player 2's view.

That's how we can work things out using orientation vectors, but we can look at the same calculation in a slightly different way, using rotation matrices. So let's do that now.

In terms of rotation matrices

-----------------------------

Instead of all this talk of orientation vectors, we can analyse the rotation aspect of our two-part transformation by using rotation matrices, which is probably a more common way of thinking about rotation and orientation. Elite's code tends to make more sense if you work with the individual orientation vectors, but if you combine all three vectors into a 3x3 matrix, they form a special kind of matrix called a "rotation matrix", which can be applied to vectors to rotate them in space; see the Wikipedia entry on rotation matrices for more details.

Using the orientation vector names from the previous section, we can say that the current ship's rotation matrix looks like this:

[ sidev_x sidev_y sidev_z ]

[ roofv_x roofv_y roofv_z ]

[ nosev_x nosev_y nosev_z ]

and player 2's rotation matrix looks like this:

[ side2v_x side2v_y side2v_z ]

[ roof2v_x roof2v_y roof2v_z ]

[ nose2v_x nose2v_y nose2v_z ]

In terms of rotation matrices, the rotation aspect of our transformation can be expressed as a multiplication by the transpose of player 2's rotation matrix. The transpose "reflects" the shape of the matrix in a "mirror line" along the diagonal from top-left to bottom-right, so it looks like this:

[ side2v_x roof2v_x nose2v_x ]

[ side2v_y roof2v_y nose2v_y ]

[ side2v_z roof2v_z nose2v_z ]

Given these two matrices, the rotation aspect of our transformation looks like this when expressed in terms of rotation matrix multiplication:

[ sidev_x sidev_y sidev_z ] [ side2v_x roof2v_x nose2v_x ]

[ roofv_x roofv_y roofv_z ] . [ side2v_y roof2v_y nose2v_y ]

[ nosev_x nosev_y nosev_z ] [ side2v_z roof2v_z nose2v_z ]

Next page | Previous page | More Lobsters retrocomputing | Headlines

Original: https://elite.bbcelite.com/hacks/two-player_elite/technical_information.html