Thankfully the document explains quite a bit about the drawing system.
First, let's have a closer look at how the level data looks like. Each level consists of 24 screens, and each screen is divided into three rows of 10 blocks each. So 30 blocks in total per screen. Blocks can be identified by their X and Y coordinates or by their block index, e.g. the block in the bottom right corner has X/Y of 9/2 or a block index of 29 (we start to count at zero).
The data structure to describe the layout of a screen is simply 30 bytes, one for each block, containing a 5-bit object id number per block. The object ids were not listed in the document, but luckily someone else had already reverse-engineered them (see page 12 of this document). I list them here for completeness sake with the names that Jordan used:
$00 = OBJID_EMPTY $01 = OBJID_FLOOR $02 = OBJID_SPIKES $03 = OBJID_POSTS $04 = OBJID_GATE $05 = OBJID_STUCK_PRESSPLATE $06 = OBJID_DOWN_PRESSPLATE $07 = OBJID_PANEL $08 = OBJID_PILLAR_BOTTOM $09 = OBJID_PILLAR_TOP $0a = OBJID_FLASK $0b = OBJID_LOOSE_FLOOR $0c = OBJID_PANEL_TOP $0d = OBJID_MIRROR $0e = OBJID_RUBBLE $0f = OBJID_UP_PRESSPLATE $10 = OBJID_EXIT1 $11 = OBJID_EXIT2 $12 = OBJID_JAW $13 = OBJID_TORCH $14 = OBJID_BLOCK $15 = OBJID_SKELETON $16 = OBJID_SWORD $17 = OBJID_BALCONY1 $18 = OBJID_BALCONY2 $19 = OBJID_ARCH_PILLAR $1a = OBJID_ARCH_SUPPORT $1b = OBJID_ARCH_SMALL $1c = OBJID_ARCH_TOP_LEFT $1d = OBJID_ARCH_TOP_RIGHT
Here's the data describing the first screen of the game (note that the values need to be AND-ed with $1f to get object ids):
$00 $00 $00 $21 $01 $21 $21 $21 $34 $34 $33 $33 $21 $23 $00 $34 $14 $14 $14 $34 $14 $14 $34 $34 $2E $23 $0B $01 $21 $34
Interestingly, the PC version has an additional object id, even though the levels are basically identical to the Apple II version. That extra type is OBJID_TORCH_WITH_RUBBLE with id $1e.
When a falling floor lands, it replaces the current block with OBJID_RUBBLE. But if that happens at a block of type OBJID_TORCH then the torch will suddenly disappear, because OBJID_RUBBLE does not include a torch. This bug has been fixed in the PC version (and probably others as well), where the block will be replaced with OBJID_TORCH_WITH_RUBBLE if it was OBJID_TORCH.
So each screen is 30 bytes and there are 24 screens, which yields a total size of 720 bytes for the whole level layout. This part of the level data is called BlueType.
There's a second set of 720 bytes per level (BlueSpec) which contains a state value byte for each block. Here the game keeps track of animation states (how far a gate has been raised, how far the spikes are extended, etc.)
All of the above has been documented already, so nothing here was really new to me. But it didn't really help me with figuring out how to draw the screens yet. I had to do more code digging to find out.
After having looked through the collision code earlier, I was able to identify a few crucial memory locations that deal with screens:
VisScrn is the currently visible screen number (1 to 24).
If NewVisScrn is not equal to VisScrn, then it will cause a new screen to be drawn (after which VisScrn is set to NewVisScrn).
There's also a routine I named
which fills in the other variables, the 8 screens surrounding the currently visible screen.
I experimentally found out that it's easy to force the game to draw a new screen, by setting NewVisScreen to the desired screen number, but when doing that the Kid was still in the old room (i.e. collision detection was performed in that one), but I quickly noticed that I also had to change KidScrn ($5b) to the new screen to fully teleport the player.
Now I was finally making a bit of progress. The code that changes NewVisScrn is triggered by
whose name was mentioned in the document. Basically the game checks if a character is exiting the screen (5B08:checkIfCharIsOffscreen) and changes CharScrn ($4b) if that's the case (5415:changeCharScrn). Then it sets NewVisScrn to CharScrn.
detects the screen change and sets ScreenHasNotBeenDrawn to 1, which indicates that the drawing code has to do a full screen refresh.
At this point, I understood enough to really find the main update loop and make sense of it. This allowed me to make my own program structure to be more like the Apple II game.
I identified mainLoop to be
and with all final labels it looks like this:
;---------------------------------- mainLoop: jsr updateAndGetRandomNumber lda #$00 sta KidStrengthDelta sta ShadStrengthDelta jsr updateInputDevices jsr isButtonPressed bpl l21a2 lda #$01 jmp initGameAndLevelImpl l21a2: jsr updateTimers jsr updateCharacters jsr activateScreenFlash jsr updateScreen jsr updateSoundEffects jsr clearSoundEffectBuffer jsr updateScreenFlash jsr updateMusic lda NextLevel cmp CurrentLevel beq mainLoop startNextLevel: jsr l27e5 ; does something special before level 2 jmp activateNextLevelOrCutscene ;----------------------------------
Back then I didn't know what most of these do, but updateScreen stood out because it did something with ScreenHasNotBeenDrawn. It checks if it's 0 or 1 and then branches, to either draw the whole screen (2439:initialDrawScreen) or just the parts that have changed in the current frame (2482:redrawScreen).
Initially I had no idea what redrawScreen did. All I cared about was initialDrawScreen. I remember that at this point I didn't stop until I had all of its sub-routines documented. It turned out to be a pretty straight forward system.
It basically scans through the blue print data of the screen (1290:iterateScreen) using the helper function (04CC:getScrnEntryInBluePrint) and calls (161E:drawBlock) for each block.
At this point it really dawned on me that I will probably have to reproduce the whole screen drawing using a bitmap. Initially I still thought that I could probably use the old C64 shortcut of using a modified character set, which means to you only have to store one or two bytes to draw a whole 8x8 pixel block on the screen. But using bitmaps meant that I not only have to write 8 times as much data, also I'll need significantly more memory as frame buffer. And it was already tight.
I finally noticed that the screen can not be nicely divided into 8x8 blocks. The height of one block row was not 64 pixels, but 63 pixels. This was because the top three pixels of the screen actually show the bottom 3 pixels of the screen above. That was something which was necessary for the player to see and break loose floors of that screen.
Of course I could have fudged it a bit. Make each row 64 pixels high, draw the status display in the screen border using sprites, use the topmost 8 pixels for that special top row. Or maybe even force a badline at the right place to shorten one character row to 7 pixels.
But what would the implications be? Would I have to change animation tables or hard-coded values to be able to climb and grab ledges if the screen rows are of a different height?
How would I animate the dynamic parts of the screen? Could I fit it all into characters for every possible screen? I didn't want to rule out the possibility of using a level editor to create completely new levels.
There were many things that I didn't know yet, so I decided to go for the safe route. Don't deviate from the original too much, until I know how it all works, and then reconsider. Better than painting myself into a corner by doing a premature optimization.
So to this day, I still don't know if character mode would've been an option. I guess I'll have to wait for someone else to try it.
Next time I'm gonna dive into how each block is actually drawn and how the game handles the isometric perspective and its visibility issues. And how I'm finally able to run through the first level.