Part 37: Hex Wars: An autopsy (Part 2)Hex War: An Autopsy: Part 2
In my last update, I spent the whole thing talking about two lines of Commodore BASIC. This was mainly because I had to do a lot of talking just to get to the point where I could talk about the lines themselves. Hopefully I can get a little farther today.
For now, I'm going to skip lines 2-4. They define two more subroutines, one of which I'll talk about later in this update, the other of which I'll talk about when it's actually called. Instead, we'll go straight to line 9, which is where we were headed at the end of part 1.
Actually, I'm going to skip over line 9 briefly, except to note that it's checking whether line 10's job has already been done and, if so, not doing it again. But what does line 10 do?
9 IF PEEK(12289) + PEEK(12290) = 212 THEN 20 10 POKE 56334, 0: POKE 1, 51: FOR I = 0 TO 2047: POKE I + 12288, PEEK(I + 53248): NEXT: POKE 1, 55 20 POKE 56334, 1: POKE 53272, 28: C4 = 53280: C0 = 53281: C5 = 646: SC = 53287: POKE 2040, 13
The first command puts a 0 into memory address 56334. This stops CIA chip 1's Timer A. CIA (Complex Interface Adaptor) 1 controls the keyboard, joystick, mouse, and Datasette (the C64's tape drive), as well as IRQs (interrupts). By turning off the timer, we've temporarily disabled most interrupts.
What's an interrupt? In simple terms, an interrupt is something that happens to interrupt the regular flow of machine operation. For instance, Commodore BASIC has an interrupt that fires off to stop (for instance) running a BASIC program long enough to check for user input. If Hex War doesn't reverse this action before it stops running (spoiler alert: it will), we'd be left looking at a READY. prompt but being unable to actually type anything.
In order for BASIC to check whether any input has been received, however, it needs to be able to see the I/O area... and the second command, putting a 51 in memory address 1, hides it, instead showing the character ROM. (The details of why 51 are kind of ugly; if you want to figure it out yourself, check the Commodore 64 memory map. Tip: 51 decimal = 00110011 binary.) If we hadn't turned off the timer, our program would crash in very short order after this command from trying, and failing, to get information from the outside world. (How short order? I had a sample program that POKEd 51 into memory address 1, then did an infinite loop of printing 'TEST'. The computer froze after printing 'TE'. It didn't get through the first command after the POKE.)
So why do we want to see the character ROM? That's what the next three statements need. The FOR-NEXT loop copies the 2048 memory addresses starting at 53248 to the 2048 memory addresses starting at 12288. This 2K of information is the first of two fonts stored in the character ROM, and by copying it into RAM, we have a starting point for the custom font Hex War uses.
The last command on line 10 puts 55 into address 1, which re-hides the character ROM and brings out the I/O area. Finally, the first command on line 20 turns Timer A back on, which means Commodore BASIC will start checking for input again.
So, backtracking to line 9. What it does is add two values from the memory area we've reserved for our custom font and skip line 10 if they add up to 212. (THEN (number) is short for THEN GOTO (number).) In theory, this means that we've already copied the font from the character ROM and don't need to do it again. In practice, there are a number of ways (albeit unlikely ones) for this to fail, so I think I'll remove that check as part of one of my updates of the program.
Back to line 20. The second command puts 28 into memory address 53272. This address has two functions: it sets what part of memory is used to store the contents of the screen, and it sets where the C64 looks for its font definitions. The value 28 in particular tells it to look for its font starting at memory address 12288 and to look for its screen starting at memory address 1024 (the default location). Now the C64 is actually using our font, even though our font is currently just a copy of the default font.
An important digression: as I said, the character ROM contains two fonts. The first only has uppercase letters, but has a variety of graphical symbols; the second turns the uppercase letters into lowercase ones and replaces some of the graphical symbols with uppercase letters. Since we've only copied the first one, if, for some reason, the computer tries to switch to the second one, the screen's going to turn to garbage - and making it switch fonts is as easy as pressing Shift and the Commodore key (Shift-Control on VICE), and works at any time! This is a bug in the original version of Hex War (though the fix is as easy as pressing Shift-Commodore again), and I'll see if I can fix it later.
Anyway. The next thing line 20 does is set some variables. C4 is set to the location of the memory address that controls the border color; C0 does the same for the background color; C5 is the location of cursor color control; and SC is the location of the color of sprite #0. As you might imagine, being able to type 'C4' instead of '53280' is a time- and space-saver.
Lastly, line 20 puts 13 into memory address 2040. 2040-2047 is the default area for sprite pointers (the area moves along with screen memory). So what does that mean?
Sprites and the Commodore 64
As I said before, the Commodore 64 has eight sprites. Sprites are graphical elements that are largely independent of the rest of the screen; you can move them around without having to redraw what was under them (and, vice versa, change what's on the screen without affecting the sprites). As you can imagine, this is extremely useful for games.
Each of the eight sprites has X and Y coordinates. (Sprites can be moved partially, or entirely, off the screen; they're hidden by the border.) You can also change their priority; low-priority sprites are drawn 'behind' the screen (i.e., only on background-color pixels), while high-priority sprites are drawn in front of it. (Lower-numbered sprites always appear in front of higher-numbered sprites. I haven't tested whether a low-priority, low-numbered sprite is drawn in front of or behind a high-priority, high-numbered sprite.)
Sprites can be turned on and off, stretched vertically and/or horizontally to double size, and can be single-color or three-color. (Strictly speaking, that's two- or four-color, but the zeroth color is always transparent.) If they're three-color, they sacrifice half their horizontal resolution. In addition, all sprites share colors 2 and 3; only color 1 is individual to a given sprite.
You can detect if a sprite has collided with the background (this is pixel-accurate, i.e. a visible pixel of a sprite is overlapping a non-background-color pixel of the screen) or whether two sprites have collided (likewise). This detection can be passive (you manually check it) or active (you can set an interrupt which will cause specific code to run the instant it happens).
Last, but definitely not least, each sprite has a pointer to the location in memory where the sprite data is actually stored. These 64 bytes (which must start at a memory address divisible by 64) contain a 24x21 bitmap of the sprite's shape (which actually only takes 63 bytes; the last one is unused). For three-color sprites, pairs of bits are used to choose which of the three colors, or transparency, will be used. (That's why they only have half the horizontal resolution. They're the same size, though; they just have chunkier pixels.)
Hex War, line 20
So what does putting 13 into 2040 do? It tells the C64 that the bitmap for sprite 0 is stored at memory address 13 * 64 = 832. Addresses 828-1019 are reserved for the Datasette buffer, which is used when reading from or writing to the tape drive - but if we're not going to actually do that, it's a safe place to stash a little data.
We're not going to stash that data quite yet, though. We have a few things we're going to do first.
Line 25's purpose is to prepare the subroutine at line 2 to work, so I'll talk first about line 25, then line 2. Line 25 is pretty straightforward: it loads eleven values into JY(0) through JY(10), which, as we've already mentioned, is an array. (If you use an array without defining it first, it's automatically defined with 11 elements, 0-10, which happens to be what we want anyway.)
2 QV = 15 - (PEEK(56320) AND 15): J = JY(QV) - 128 * ((PEEK(56320) AND 16) = 0): RETURN 25 FOR A = 0 TO 10: READ JY(A): NEXT: DATA 0, 1, 5, 0, 7, 8, 6, 0, 3, 2, 4
Why those numbers? Well, that's where line 2 comes in. But first:
The Commodore 64 can have two joysticks plugged in. It uses the same connectors that the Atari 2600 used; in these days, it was a popular choice for 8-bit computers. That didn't mean you necessarily used an Atari stick, as even before the C64 came out, lots of people were making third-party sticks for the Atari, so you could grab one of those.
The Atari stick, and its various third-party copies, were digital sticks; in other words, they were basically modern D-pads. They could read nine positions (horizontal, vertical, diagonal, or centered) and had a single digital (on/off) fire button.
So how do you read a joystick in BASIC? PEEKs, naturally. To read a joystick in port 1, you looked at memory address 56321; to read one in port 2, you used 56320. The information you gleaned from these was identical, at least for joysticks. (Naturally, these values were also used for other things, because every bit counts, and a digital joystick only needs five of the eight.)
The bits had the following meanings:
- Bit #0 (1): if 0, the joystick is being pushed up.
- Bit #1 (2): if 0, the joystick is being pushed down.
- Bit #2 (4): if 0, the joystick is being pushed left.
- Bit #3 (8): if 0, the joystick is being pushed right.
- Bit #4 (16): if 0, the joystick's fire button is being pushed.
As you may have guessed by now, line 2 is a joystick-reading subroutine. Let's break it down.
Inside the first statement of line 2, we have PEEK(56320), which gives us the status of the joystick in port 2. (I prefer using a stick in port 2 because a stick in port 1 actually interferes with the keyboard. Pressing the fire button, for instance, is interpreted as pressing the space bar. And no, the directions don't correspond to the arrow keys. That would make sense.)
We then perform a logical AND with 15. AND, when given two numbers as input, breaks them down to their binary representations, compares them, and returns a number whose binary representation's bits are only 1 when both of the two numbers' binary representations' bits were 1.
Lemme give you an example. Let's suppose I'm holding up and fire on joystick 2. PEEK(56320) gives me 110. 110 AND 15 is:
110 = 01101110 = 64 + 32 + 8 + 4 + 2
015 = 00001111 = 8 + 4 + 2 + 1
014 = 00001110 = 8 + 4 + 2
So we have the number 14. Now we subtract that from 15, so QV is assigned the value 1.
The second statement on line 2 is even more complicated, but we can break it down the same way. PEEK(56320) AND 16 is 0, because the fire button is being held down.
Then we do something weird. What does saying '0 = 0' even mean? Well, it's not an assignment; it's a logical equality test. (Many languages have different symbols for 'assign a value to this' (typically '=') and 'test if these two values are equal' (typically '==') for just this reason.)
The way logical equalities in Commodore BASIC work is, if the two values are equal, the value of the equality is -1. (Because of the way the computer stores signed integers, this is a value whose bits are all 1.) If they are not equal, the value of the expression is 0. So, since 0 does equal 0, the value of this expression is -1.
We've now reduced the statement to J = JY(1) - 128 * (-1), or J = JY(1) + 128. We assigned 1 to JY(1), so J is given the value 129. We then RETURN to whatever part of the program called the subroutine.
So how does this make reading the joystick any easier? Well, consider the nine possible values of PEEK(56320) (with the fire button unpressed, and omitting simultaneous up and down or left and right, which wouldn't be possible on a typical joystick):
- Neutral stick: 127. QV = 0. JY(0) = 0. J = 0.
- Stick up: 126. QV = 1. JY(1) = 1. J = 1.
- Stick down: 125. QV = 2. JY(2) = 5. J = 5.
- Stick left: 123. QV = 4. JY(4) = 7. J = 7.
- Stick left and up: 122. QV = 5. JY(5) = 8. J = 8.
- Stick left and down: 121. QV = 6. JY(6) = 6. J = 6.
- Stick right: 119. QV = 8. JY(8) = 3. J = 3.
- Stick right and up: 118. QV = 9. JY(9) = 2. J = 2.
- Stick right and down: 117. QV = 10. JY(10) = 4.
8 1 2
6 5 4
That's right; we've converted arbitrary bits to an easier-to-understand number. And what about the fire button? If it's pressed, just add 128 to the number here.
There are other approaches to reading the joystick, incidentally. This approach works well if you always care exactly which direction the stick is pressed in, but if the program has to ask 'is the stick being pushed up at all', for instance, it has to check whether this number is 1, 2, 8, 129, 130, or 136. An alternate approach is to simply invert the bits - J = 255 - PEEK(56320) = and then use, for instance, J AND 1 to know if the joystick is being pushed up. (Why invert the bits? While logical expressions return -1 for 'true', any non-zero value is also treated as true, so I can just say IF J AND 1 THEN (whatever) instead of IF J AND 1 = 1 THEN (whatever). Without inverting the bits, I'd have to use IF J AND 0 = 0 THEN (whatever). It's a matter of a little extra typing in one spot to save typing in several others.)
So we've got us a joystick reading routine! And we've also copied a font and gotten ready to use it, as well as set aside some space for a sprite. We haven't actually defined the sprite or changed the font, though. And we won't for a bit longer, because not only is this update at an end, but we're going to be doing some other things in the next update. Tune in then!