Lobsters retrocomputing - 03 Sep 2026
Page 2 of 5
Finally, we also need to update the in-flight messaging system to cater for messages in both player views. As there are a few variables used to store the current message details (so it can be easily erased), it's easier just to duplicate the MESS routine into Player2MESS, so MESS prints in-flight messages in player 1's view, and Player2MESS does the same for player 2. I ended up duplicating quite a few aspects of the game for player 2 in this way; see the miscellaneous section for more details.
Now that we have drawing routines that can cater for the two split-screen player views, let's talk about how we can draw the contents of each player's space view.
Drawing player 1's space view
-----------------------------
As discussed in the overview, the heart of two-player Elite is the exact same player-centric local bubble model as in the original single-player game, with player 1 at the centre of the universe. Drawing the top space view for player 1 is therefore fairly easy, at least in concept; we just draw the game screen as usual, showing everything from the perspective of player 1, and all we need to do is clip what we draw so it fits into the half-height space view at the top of the screen.
This makes it sound a lot easier than it is in practice, but the concept shouldn't be too difficult to grasp. Drawing player 2's space view, on the other hand, is considerably more challenging, and is covered in the next section, but for this section let's stick to player 1's view - the one showing a Thargoid and the sun in this screenshot:
Before describing how two-player Elite works, let's recap how single-player Elite stores its environment, and in particular the ships and other objects in the local bubble of universe that we want to draw in the space view. This is all described in detail in the deep dives on the local bubble of universe and ship data blocks, but here's a brief summary.
The game has a fixed number of ship slots, with 12 in the BBC Micro version and 20 in the 6502 Second Processor version (two-player Elite is based on the latter). Each object in the local bubble occupies one slot, with the planet in slot #0, either the sun or the space station in slot #1, and then all the various ships and missiles and asteroids in slots #2 and up.
Each slot is managed via the FRIN table, which contains one byte per slot; a zero entry indicates an empty slot, while a non-zero entry indicates either a ship, planet, sun or station (in which case FRIN contains the ship type). Each occupied slot also has an associated ship data block, which lives in the K% workspace, and one of those bits of data is the address of the ship's line heap, which is used to store the coordinates of the ship's on-screen wireframe lines, so the lines can quickly be redrawn using EOR logic to remove the ship from the screen.
All space coordinates in single-player Elite are relative to the player, who lives at the origin with coordinates (0, 0, 0). The z-axis goes into the screen, so that's pointing straight out of the nose of the player's ship, while the x-axis goes from left to right and the y-axis points up. (Note that the BBC Micro's screen y-coordinates go the other way and increase as you move down the screen, but we're talking about 3D space coordinates here, and they increase as you move up in space.)
Finally, note that in this context, "ship" is used to refer to anything with a slot, so that includes the sun, the planet, the station or non-ship objects like asteroids or cargo canisters. It's a lot easier to say "ship" than "ship, planet, sun, station etc." every time.
We keep this model for two-player Elite, but in a reduced manner. Because two-player Elite is a deep space dogfighting game, we don't come across any space stations, so slot #1 is always allocated to the sun. And we also strictly limit the number of objects to avoid slow-downs, with a maximum of one in-flight missile per player giving a limit of two in-flight missiles at any one time.
The ship slots in two-player Elite therefore look like this, with player 1 at the centre of the universe:
- Slot #0 = Planet
- Slot #1 = Sun
- Slot #2 = Player 2's ship
- Slot #3 = Missile 1
- Slot #4 = Missile 2
That's it - that's the local bubble of universe for two-player Elite. We may have anything from 0 to 2 missiles spawned, but we always have the planet, the sun and player 2's ship.
This bubble works nicely for drawing player 1's space view, as all the space coordinates are relative to player 1 at the origin, just as in the original single-player game. But how do we take this bubble structure and use it to draw player 2's space view?
That's a simple question with a complicated answer, so first let's look at how player 1's view is drawn and see if that helps. The details can be found in the deep dive on program flow of the main game loop, but to save you wading through all that, let's concentrate on the ship-processing code at the heart of the game loop.
Every iteration of the main loop, the game works through each of the ship slots, one slot at a time, and it applies movement and rotation to the ship we are processing (the "current ship"). This movement is affected not only by how the player is moving in space, but also by the current ship's own rotation, speed and acceleration. Tactics are also applied at this point, so pirates will attack and traders will mind their own business, for example. Once the current ship has been updated, then the new data is stored in the ship's data block, and the ship is redrawn on-screen. This latter step is done in two parts, first by redrawing the existing on-screen lines using EOR logic and the coordinates in the ship line heap, and then by drawing the new lines on-screen (again using EOR logic to merge with the existing screen contents) and storing the new coordinates in the ship line heap.
This ship-drawing loop manages the ships, the sun and the planet in the space view in single-player Elite, with the actual drawing being done by the LL9 routine. This is called in part 12 of the main flight loop, and it caters for the planet, the sun and the ships, so LL9 ends up being called once for each populated ship slot. Once we've finished going through the ship slots, the game calls the STARS routine to update the stardust, and that's how the space view is drawn in single-player Elite.
For two-player Elite, then, we can use the same loop for drawing player 1's space view, with LL9 and STARS taking care of the ships and the stardust. We just need a similar system for drawing player 2's view, ideally without adding too much overhead.
Let's take a look at that next.
Drawing player 2's space view
-----------------------------
The core approach in two-player Elite is to treat ship slots #0 to #4 as the single source of truth for the local bubble, and to draw that same bubble from the perspective of player 2 in player 2's space view. To keep the flight loop as unchanged as possible, we add a new routine, DrawPlayer2View, which we call for each ship slot, just after LL9 has drawn that ship in player 1's space view.
DrawPlayer2View, as its name suggests, draws the current ship, but it does it in player 2's view and from the point of view of player 2. DrawPlayer2View is the core of two-player Elite, and you can see it in the raw source by searching for ".DrawPlayer2View".
To simplify things a bit, you can think of DrawPlayer2View as a large, six-part wrapper around yet another call to LL9 to draw the current ship, but before drawing the ship, we convert the current ship's data block from the default perspective (i.e. the view from player 1's ship) into a different frame of reference (i.e. the view from player 2's ship). The call to LL9 then draws the current ship into player 2's space view without us needing to make any changes to LL9 itself.
DrawPlayer2View is split into six parts:
- Part 1 processes byte #31 of the ship data block, as this contains data that isn't necessarily the same for each ship in the two different player views. For example, a ship might be visible in one view but not visible in another, and as that information is stored in bit 3 of byte #31, we need to manage this data byte differently for each ship in each view.
- Part 2 applies special rules to the planet and sun when they are on-screen, as described in the section on cheating with the sun and planet.
- Part 3 is the most important part of two-player Elite, as it calculates the current ship's coordinates and orientation in player 2's frame of reference, so it can be drawn in player 2's space view. We'll talk about this in the remainder of this section, and we'll explore the maths in the sections on the geometry behind player 2's view and the arithmetic behind player 2's view.
- Part 4 is nice and short, but it's important, as it draws the current ship in player 2's space view. It starts by setting bit 7 of drawPlayerView to ensure the ship is drawn in player 2's view. It then calls PLUT to switch to the correct directional view - front, rear, left or right - just as we do in single-player Elite (see the deep dive on flipping axes between space views for details). And finally it calls LL9 to draw the current ship in player 2's view.
- Part 5 deals with lasers and targeting, as described in the section on target calculations.
- Part 6 returns to the ship data to the state it was in when we called DrawPlayer2View, so the main loop can continue on as if nothing has happened.
This approach makes sense until we need to draw the ship in slot #2. Slot #2 contains player 2's ship, but player 2 can't see their own ship, so there's nothing to draw. So when we are processing the current ship in slot #2, DrawPlayer2View instead draws player 1's ship from the perspective of player 2, as player 1's ship doesn't actually have a ship slot (because in the local bubble, player 1 is always at the origin and is always aligned with the axes, so we don't need to store its coordinates, orientation and so on). As a bonus, the maths we need to do when working out where player 1's ship appears in player 2's space view is a simplified version of the maths we need to do for the other slots; see the section on the geometry behind player 2's view for more on this.
For the rest of this section, let's examine part 3 of DrawPlayer2View in more detail, as this is where the magic lives. By this point we are processing the current ship and have drawn it in player 1's view. As a reminder, the slots are set up as follows:
- Slot #0 = Planet
- Slot #1 = Sun
- Slot #2 = Player 2's ship
- Slot #3 = Missile 1
- Slot #4 = Missile 2
So given the ship data for the current ship, we now we need to draw the ship from the perspective of player 2, and in player 2's space view.
If you look at the ship data for a typical ship, it's mostly coordinates and orientation vectors; see the deep dive on ship data blocks for details. The coordinates are the (x, y, z) space coordinates of the ship relative to player 1, while the orientation vectors define the direction in which the ship is pointing, stored as three vectors - nosev, roofv and sidev - that point out of the nose, roof and right side of the ship respectively. These vectors are said to be orthonormal, which just means that the vectors are orthogonal (i.e. they are perpendicular to each other), and normal (i.e. each of the vectors has length 1). See the deep dives on orientation vectors and tidying orthonormal vectors for more information on these vectors.
So out of 37 bytes in each ship data block, the first 27 bytes define the ship's position and orientation in space, all of them from the perspective of player 1. As player 1 and player 2 can't be at the same point in space, we know that these 27 bytes will be different for this ship when viewed from the perspective of player 2.
We'll look at exactly how they differ in the next section, but in terms of DrawPlayer2View, our first step is to make a copy of the ship data for the ship we are trying to draw, because we're going to have to change most of it when drawing that ship in player 2's view. To make things simple, we can duplicate the current ship's data from slot #n into slot #n+10, like this:
- Slot #10 = Planet from player 2's perspective
- Slot #11 = Sun from player 2's perspective
- Slot #12 = Player 1's ship from player 2's perspective
- Slot #13 = Missile 1 from player 2's perspective
- Slot #14 = Missile 2 from player 2's perspective
So, for example, when we are processing slot #1 in the main game loop, which contains the sun, DrawPlayer2View copies the sun's ship data into slot #11; similarly, player 2's ship gets copied into slot #12 for processing in DrawPlayer2View, and so on. This duplication process means we can apply our transformation maths to the copy of the current ship's data to convert it into the coordinates and orientation for player 2's view, and we can simply pass this higher slot number to LL9 to draw the ship, and it will all just work. Skip to the section on the geometry behind player 2's view to read about this transformation process, as it deserves a section all of its own.
The ship data in the higher slot number does get used once more after the ship has been drawn, but it isn't until the next time the main loop processes this slot, when we need to update the ship in player 2's view. Before the higher slot ship data is overwritten, part 1 of DrawPlayer2View uses it to remove the ship from player 2's scanner (i.e. the yellow ship stick), as redrawing the ship stick in its current position with EOR logic will remove it. Then we can copy the lower slot number data into the higher slot number again, overwriting what's there, and the whole process starts again.
There are some important caveats in this copying process. As mentioned above, byte #31 of the ship's data block contains a number of flags that don't make sense when blindly applied to player 2's perspective, so instead we store byte #31 separately for slots #2 to #4 (these are stored in the three bytes at player1INWK31). Part 1 of DrawPlayer2View therefore starts by looking at byte #31 for the current ship; by this point the current ship's data is in the zero page INWK workspace, so DrawPlayer2View checks the flags in INWK+31 and copies any relevant ones into the corresponding byte in player1INWK31.
For example, if a missile has just exploded then bit 7 of INWK+31 will be set, so we will want to copy that over into player1INWK31 so the missile explodes in player 2's view as well as in player 1's view; but if a ship is visible in player 1's space view then that has no bearing on whether it will also be visible in player 2's space view, so we don't want to copy over bit 3 of INWK+31 (which records this fact). Instead we use bit 3 from player1INWK31 to keep track of whether a ship is on-screen in player 2's view.
The other important difference in ship data between the lower-numbered and higher-numbered slots is the address of the ship line heap. The ship line heap is very simple - it contains sets of four coordinates, each of which describes a line in that ship's on-screen depiction. To draw the ship we simply work through the heap, drawing each line, and to remove the ship from the screen, we repeat the process using EOR logic. You can read all about this in the deep dive on drawing ships.
Obviously, the same ship will look completely different in player 1's view compared to player 2's view, so we need to maintain separate ship line heaps for the lower-numbered slots and the higher-numbered slots. To this end, when we duplicate the ship data for a lower-numbered slot into a higher-numbered slot, then as we do the duplication, we subtract &2000 from the ship line heap address in bytes #33 and #34 of the ship data block. The 6502 Second Processor version of Elite has quite a generous memory allocation to the ship heap, as you can see in the 6502 Second Processor Elite memory map; the heap stretches downwards from &D000 to &84E4, so that's &4BCC bytes. We're only using the top part of the ship heap for player 1's ship and two missiles (as the planet and sun have their own line heaps), so the maximum heap size required is 157 bytes for the player ship (based on the Cobra Mk III, which has the largest requirement), plus 85 bytes for each missile, giving a total of 327 bytes, or &147. Spacing out the two views' ship heaps by &2000 bytes is therefore complete overkill, but it's better to be safe than sorry.
The planet has its own ball line heaps at LSX2 and LSY2 that are populated by the BLINE routine (see the deep dive on the ball line heap for details). In order to support two different space views with two different-looking planets, we therefore need to allocate memory to a second pair of line heaps for the planet in player 2's view, which we can call LSX2a and LSY2a. We can then reuse a method that I first used in the anaglyph routines in Elite 3D to support different right-eye and left-eye views. To get BLINE to work with the correct ball line heap, we can recode the routine to look up all heap-related addresses from vectors, which get set by the SetPlayerBallLine routine, depending on the current value of drawPlayerView. Specifically, the LSX2S(1 0) vector points to either LSX2 or LSX2a, the LSY2S(1 0) vector points to either LSY2 or LSY2a, and the LSPS(1 0) vector points to either LSP or LSPa, so we can use the same BLINE routine to draw the ball line for each of the player views individually, while storing the results in the correct ball line heap.
The sun also has its own line heap, but this time we don't need to do any duplication, as the sun stores its lines as one byte for each of the 192 raster lines in the space view. We can therefore simply use the first half of the existing heap for the top space view and the second half for the bottom space view, and the only bit we need to duplicate is the centre of the sun's x-coordinate in SUNX, as that can obviously be different for the sun in each of the two views. Again, we use a vector approach, so the LSOS(1 0) vector points to either LSO or LSOa, and the SUNXS(1 0) vector points to SUNX or SUNXa. This gets set in SetPlayerSunHeap, again according to the current value of drawPlayerView.
Next page | Previous page | More Lobsters retrocomputing | Headlines
Original: https://elite.bbcelite.com/hacks/two-player_elite/technical_information.html