Lobsters retrocomputing - 03 Sep 2026
Page 1 of 5
How I converted BBC Micro Elite into a two-player game
Under the bonnet, BBC Micro Elite is a stubbornly single-player game. This makes perfect sense, as that's how the game was designed, but it does mean that it's a bit of a challenge for anyone wanting to coax a second player out of Bell and Braben's elegant code.
This article describes how I built two-player Elite. There's a lot to say, so I've split it up into a number of sections that cover the development process in the order in which I tackled it. You might like to read them from start to finish, but you can also click on them to jump down to the relevant section:
As with all my hacks, two-player Elite takes the original game's source code and modifies it; it is not a brand new game, it is a hack of the original. In the case of two-player Elite, the original game is the 6502 Second Processor version of Elite, with the flicker-free hack already applied. I then added all the various modifications required to add a second player on top of that base.
You can see all this in the project's GitHub repository. If you search the source code for "Mod:" then you will find every change that I've made to the original 6502 Second Processor version of Elite to get to two-player Elite. The changes are split between the parasite and I/O processor sources, as appropriate, and every change is documented to the same level of detail as the rest of the original game's source code (in other words, every single line is explained in detail).
I hope you enjoy reading about two-player Elite as much as I enjoyed hacking it into existence.
An overview of how two-player Elite works
-----------------------------------------
There's a clever conceit at the heart of 8-bit Elite that makes life considerably simpler for anyone working with the game code. It is this: the player is always at the centre of the universe.
For example, when the player moves forwards, they don't actually move at all; instead, everything else in the local bubble moves backwards. And when a player rolls to the right, they don't actually rotate; instead, everything in the local bubble rotates to the left around them. You can read all about this in the deep dive on rotating the universe, and it's key to understanding two-player Elite.
Always having the player at the origin makes certain aspects of the game code a lot simpler. For example, it's almost trivial to work out if an enemy ship is in the player's laser sights; we first check whether the enemy is in front of the player (i.e. z > 0 for the enemy's z-coordinate), and if it is, we check whether it is close enough to the z-axis to be within the enemy ship's targetable area (i.e. x^2 + y^2 < t for the enemy's x- and y-coordinates, with t being the targetable area from the enemy ship's blueprint). Because the z-axis points straight out of the front of the player's ship, out of the ship's nose and directly along the laser lines, we don't need to care about rotation or orientation, we just do some simple multiplication and the result is very accurate. You can read all about this in the deep dive on being in the crosshairs.
Unfortunately, this conceit makes it pretty difficult to add a second player to Elite. If the whole universe is centred on one player, then it can't also be centred on another player, so how do we address this?
One approach might be to run two separate instances of Elite, one for each ship, and have them talk to each other somehow; in essence, building a networked version of Elite, just within the same machine. This might work, but it would require a lot of memory and a lot of CPU power, and it would get pretty complicated pretty quickly.
Instead, two-player Elite fully embraces the player-centric conceit at the heart of the original game, and effectively bolts a second player onto this tried and trusted game engine. Let's consider the in-game two-player Elite screen:
The local bubble is still centred on one player; let's call them player 1, with the top space view showing the view from player 1's cockpit. In a very real sense, the top view is completely standard Elite, just cropped into a smaller part of the screen. When player 1 rotates or accelerates, they don't move, but the whole local bubble moves around them instead; the top space view is normal Elite in pretty much every way that matters.
Player 2, then, is simply another ship that is spawned into player 1's local bubble. Player 2 isn't at the centre of the universe; instead, they are just like NPC ships in the original game, so when player 1 rotates or moves, the whole bubble - including player 2's ship and anything else in the vicinity (like the planet or sun) - rotates and moves in the opposite direction, just like all the non-player ships in the original.
The difference is that player 2's ship is not controlled by the game's tactics routine; instead, it is controlled by player 2's joystick and secondary flight keys on the keyboard. So when the human player 2 rotates the joystick or presses the "go faster" button, player 2's ship rotates or moves within player 1's local bubble. Player 2's ship doesn't rotate the universe around itself, it just moves in space as you would expect.
This isn't too much of a leap - after all, NPC ships in the original game work in this way, so moving NPC ships by joystick rather than by algorithm isn't too hard to imagine. Indeed, you can configure player 2 to be an AI Pilot in two-player Elite, in which case the code controlling player 2's ship just switches back to the original tactics routines rather than reading the joystick and keyboard.
So we have a model that is familiar, in that we have a player 1-centric universe that contains the planet, the sun, player 2's ship and up to two missiles. Player 1's space view is easy enough to draw, as player 2 is just another ship in the local bubble, so we can draw the top space view as normal (just with the required cropping).
This defines the main challenge of the two-player Elite hack: how do we draw all of this into the bottom space view, so that everything is correct from the point of view of player 2? This is where the complexity arrives, so I've split the answer up into a number of sections:
- We start by talking about how to draw player 1's view, as this gives us a chance to revisit how single-player Elite works. See the sections on the split space view and drawing player 1's space view for details.
- In order to draw player 2's view, we first create duplicates of the sun, planet, ships and missiles in the local bubble. This is described in the section on drawing player 2's space view.
- Once we've got our duplicates, we need to transform each of them from player 1's frame of reference (i.e. with player 1 at the origin and the axes aligned with player 1's ship) into player 2's frame of reference (i.e. with player 2 at the origin and the axes aligned with player 2's ship). This is the core part of two-player Elite, and is detailed in the sections on the geometry behind player 2's view and the arithmetic behind player 2's view.
- A pretty vital aspect of dogfighting in space is the ability to target and shoot your opponent. As discussed above, this is pretty easy for player 1, but it's a bit more involved for player 2, so this is explained in the section on target calculations
- Finally, all is not what it seems... of course. Accuracy and speed are constant challenges when working with floating point geometry on an 8-bit CPU, and I confess to using some smoke and mirrors to make things appear a bit more stable than they actually are. I talk about these in the section on cheating with the sun and planet.
There are also sections on some of the more straightforward aspects, such as the responsive control system, and to round it off there's a miscellaneous section for anything else worth talking about.
Let's start with a look at splitting the space view in two, which was the first task I tackled... but before we get stuck in, here's a quick note about the bit-based flags that I'm using in two-player Elite.
A note on bit-based flags
-------------------------
Two-player Elite contains a lot of new variables, and a lot of these are flags. For example, the split screen is controlled by this flag, which we look at in the next section:
- splitScreen controls the split-screen effect:
- Bit 7 set = draw the space view as a split screen
- Bit 7 clear = full screen
You'll notice that this variable uses bit 7 to store its state. This approach isn't that popular in Elite - Bell and Braben's code tends to prefer flags that are either zero or non-zero, and which can be tested with an LDA and BEQ combination. But for my own coding I've been more influenced by Geoff Crammond, who over the course of Aviator, Revs and The Sentinel, settled on using bit 7 as the flag (and possibly bit 6 as well). The advantage of using bit 7 is that we can test the state of the flag without using any registers, as the BIT instruction sets the N and V flags to bits 7 and 6 of its operand. So instead of testing whether our flag variable is non-zero, like this:
LDA flagVariable \ Set A to the value of the flag variable
BNE flagIsSet \ Jump to flagIsSet if A is non-zero
or zero, like this:
LDA flagVariable \ Set A to the value of the flag variable
BEQ flagIsNotSet \ Jump to flagIsNotSet if A is zero
we can do the following to test bit 7 being set, without needing to use a register:
BIT flagVariable \ Set the N flag to bit 7 of the flag variable
BMI flagIsSet \ Jump to flagIsSet if the N flag is set
or we can do this to test bit 7 being clear:
BIT flagVariable \ Set the N flag to bit 7 of the flag variable
BPL flagIsNotSet \ Jump to flagIsNotSet if the N flag is clear
Similarly, we can do the following to test whether bit 6 is set:
BIT flagVariable \ Set the V flag to bit 6 of the flag variable
BVS flagIsSet \ Jump to flagIsSet if the V flag is set
or this to test whether bit 6 is clear:
BIT flagVariable \ Set the V flag to bit 6 of the flag variable
BVC flagIsNotSet \ Jump to flagIsNotSet if the V flag is clear
Not only that, but we can set the flag in bit 7 without needing to use a register, like this:
SEC \ Set the C flag
ROR flagVariable \ Rotate the C flag into bit 7 of the flag variable
and we can clear it in a similar fashion like this:
CLC \ Clear the C flag
ROR flagVariable \ Rotate the C flag into bit 7 of the flag variable
or, if we're only using bit 7 of the flag variable and know that the other bits are clear (and in particular bit 6), we can do this:
ASL flagVariable \ Shift bit 6 (clear) into bit 7 of the flag variable
And because we are running the main game code on the 6502 Second Processor with the extra instructions of the 65C02 CPU, we can clear both flags in just one instruction, like this:
STZ flagVariable \ Clear bits 6 and 7 of the flag variable
You can also use these flag variables to store previous flag values by shifting right (so bit 6 inherits the previous value of bit 7, for example), but that's getting really deep into Geoff Crammond territory. For two-player Elite, I only use bits 6 and 7 as independent flags, so that's the kind of flag logic you'll see throughout the code modifications.
Splitting the space view
------------------------
Compared to some of the other challenges in two-player Elite, splitting the space view into two parts is relatively straightforward. There are two new variables that control the screen-splitting process, and they work like this:
- splitScreen controls the split-screen effect:
- Bit 7 set = draw the space view as a split screen
- Bit 7 clear = full screen
- drawPlayerView determines which player's view to draw in the split screen:
- Bit 7 set = draw player 1's view (top)
- Bit 7 clear = draw player 2's view (bottom)
Given these two flag variables, we can update any drawing calculations to draw into the correct half of the screen. We only need to make changes to the drawing routines for when bit 7 of splitScreen is set, in which case we need to update the y-coordinate of the pixel that we are drawing to point to the correct space view, according to the value of drawPlayerView.
To make this a bit easier we can use the configuration variable Y from the original source, which is set to half the height of the space view (so for the BBC Micro, Y is set to 96 pixels, as the full space view is 192 pixels high); I will refer to this variable as #Y to avoid confusing it with the 6502's Y register. So each of the two space views in the following screenshot is #Y pixels tall, and the top of the dashboard is at y-coordinate #Y*2:
We can then calculate the following:
- If we are drawing player 1's view in the top part of the space view, then subtract #Y/2 from the y-coordinate. This moves the whole scene upwards so the centre of the view (at the laser sights) is in the middle of the top half. Then we check whether the pixel fits into the top half of the screen (i.e. into the range 0 <= y < #Y) and if it does, we plot it.
- If we are drawing player 2's view in the bottom part of the space view, then add #Y/2 to the y-coordinate. This moves the whole scene down so the centre of the view (at the laser sights) is in the middle of the bottom half. Then we check whether the pixel fits into the bottom half of the screen (i.e. into the range #Y <= y < #2*Y) and if it does, we plot it.
This calculation occurs in a number of routine, all of which have been updated in two-player Elite to draw into either player 1's view or player 2's view, according to the value of drawPlayerView:
- PIXEL2 for drawing stardust
- LASLI for drawing the big, red laser lines at the bottom of the space view
- DOEXP for drawing explosions
- PLANET for drawing the planet
- SUN for drawing the sun
- SHPPT for drawing distant ships as dots
- SIGHT for drawing the laser crosshairs
- LL145 for line-clipping in circles and ships
The last routine is worth explaining a bit further, as the line-clipping routine at LL145 is applied to every line that's drawn as part of a ship wireframe or a circle, so it's used when drawing ships, planets, launch tunnels and so on. You can read all about how it works in the deep dive on line-clipping, but suffice to say it's fairly complicated and involves some relatively slow arithmetic.
For two-player Elite, we need to extend LL145 to cope with two scenarios:
- We still need the original, full-screen line-clipping routine for the launch tunnel, or in case we want to display full-screen ships on the title screen or circles on the chart screens (the charts are disabled in two-player Elite and the title screen actually clips to player 1's space view, but at least they could be easily reinstated if required).
- We need a new version that clips lines to either one of the two-player views, so we can draw ships and planets in just one half of the space view without them bleeding into the other.
I did briefly try reworking LL145 to cope with the split-screen player views, but it didn't go too well, so to avoid getting bogged down, I added a cheeky hack that worked so well I kept it. Here's how it works.
If the screen is normal and not split into two player views (i.e. when bit 7 of splitScreen is clear) then we run LL145 as normal. But if the screen is split then we use the following approach to move the line up the screen to be centred in player 1's space view, where we clip the line to the half-height view; and then, if we are drawing player 2's view, we move the clipped line down into the bottom half of the split screen. As the line's coordinates are 16-bit signed numbers that use the game's extended screen coordinates, we can move the line up the screen using normal SBC instructions, using the carry flag as the 6502 intended.
This is how the algorithm works:
- We start by subtracting #Y/2 from the line's two 16-bit y-coordinates to move the line up the screen by half the height of player 1's space view, so the line is now in the correct position for player 1 (though it hasn't been clipped yet).
- We now want to clip this newly positioned line to fit into player 1's space view, so that's the y-coordinate range 0 to #Y/2. The standard LL145 routine clips to the range 0 to #Y, so instead of just calling LL145 directly, we do the following:
- Double the line's 16-bit y-coordinates with a simple left-shift, which stretches the line in the y-axis to double its height on-screen. The top of the screen is the zero y-coordinate, so this is a bit like drawing our line on a stretchy sheet of rubber that's the same size as player 1's space view, and then gripping the sheet at the top and bottom and pulling the bottom down until the sheet is double height, i.e. the height of the full, two-player space view.
- We then run LL145 as normal, to clip the lines to 0 <= y < #Y and 0 <= x < 256. This clips our double-height line to the bounds of the full space view, so that's twice the height of the player 1 space view. If you like, this is clipping our line to the edges of the stretched sheet of rubber.
- Finally we halve the line's 16-bit y-coordinates with a simple right-shift, which is like us letting go of the bottom part of the rubber sheet, so it springs back to being the same shape as player 1's space view, but with the line clipped to the top half of the full, two-player space view.
- If we are drawing player 2's view (i.e. bit 7 of drawPlayerView is set), we finish off by adding #Y to the y-coordinates of the clipped line to move the whole clipped line down into the bottom half of the space view.
The overhead of this extra code is relatively minor, as it only requires addition, subtraction and shifting, and it leaves us with a line-clipping routine that works in both the original screen and the split screen without needing to change the original LL145 routine at all.
There are a few other areas where we need to cater for the split screen. In the original game, whenever we change the space view (front, rear, left, right) or switch to an information screen like the charts or status mode screen, then the TTX66 routine clears the whole top part of the screen, draws the yellow border and then displays the relevant information. This can't happen in two-player Elite, as otherwise one player changing views would affect the other player's view, so we need to extend TTX66 to cater for this; the easiest way is to add a couple of additional text control codes to the TT26 text-printing routine in the I/O processor, so control code 14 clears player 1's view and control code 15 clears player 2's view. This means we can clear each screen by simply printing the relevant control code, which gets interpreted by the I/O processor as a screen-clearing command.
Next page | More Lobsters retrocomputing | Headlines
Original: https://elite.bbcelite.com/hacks/two-player_elite/technical_information.html