It's time to talk about structure and states. Let's crack open the good old yEd and make some sense of this.
The game will open with a title screen with input selection. If we have the space, we can show a logo here. The screen will just ask the player to press 1, 2 or 3 depending on their input style of choice, which will be stored in the inputmode variable, after which the player is thrown into the level selection.
In the level select the player can browse through the available levels using sideways movement, with a preview of the level and level name shown. Maybe with how many gems were collected. From here, the player gets into the game.
Inside the game, the player can either be actively playing, dead, or done. From game's structure point of view, both of the latter two are identical: keep running simulation, don't accept movement input.
Since it's possible to end up in a situation where you're neither dead nor can move, we'll make it so hitting a button will return us to the level selection. The same can be done in all the game states, so we'll just make it a global option.
There's a bunch of things we need to do, so let's get cracking..
We need a way to tell the player which keys to press to pick the input device. While I'm planning on making a text output routine eventually, we'll just bite the bullet and load a whole screen image. That will cost us 6912 bytes, which sounds nuts in our scale especially for a screen that's seen once at the beginning and then never again.
We could store this in a compressed form in contended memory and use a decompressor (like zx7) to decompress it on the fly, or even load it to display memory from tape before running our binary. For now, though, I think we can just live with it. We still have the room for it..
keyselect_scr: INCBIN "keyselect.scr"
I quickly made the image using my image spectrumizer (github). The image isn't great, but it does the job.
; Entry: key select ld a, 0 out (0xfe), a ld de, SCREEN ld hl, keyselect_scr ld bc, (256/8)*192+32*24 ldir
Right after we've set up the ISR, we set the border color to black, and set up registers for LDIR to copy the image to screen, covering both bitmap and attribute area. There's a possibility the bitmap will show before the attributes are done, which could be solved by clearing the attributes to black first, but that's a minor issue.
LDIR is not the fastest way to copy data on the z80, but it's definitely the most compact. Faster ways include having strings of LDI (the R in LDIR is for repeat - LDI does a single step) and abusing the stack.
inputselectloop: call scaninputs ld a, (keydata + 3) bit 0, a jr z, select_1 bit 1, a jr z, select_2 bit 2, a jr z, select_3 jp inputselectloop select_1: ld a, 0 ld (inputmode), a jp levelselect select_2: ld a, 1 ld (inputmode), a jp levelselect select_3: ld a, 2 ld (inputmode), a jp levelselect
Here's one place where the fact that we read all the inputs instead of just what we needed comes in handy. We re-use the scaninputs function to update the keydata array, check if any of the keys 1, 2 or 3 are pressed, update the inputmode variable as appropriate and jump to level select.
map: BLOCK 16*12,0 levels: db 2,2,6,3,5,2,2,2,2,2,2,2,2,2,2,2 db 2,5,6,3,2,2,6,2,2,2,2,2,2,2,2,2 ...
For level select to make sense, we need separate map and level storage. This change is simple enough. We still have just one level, though. I initialized the map as a block of zero bytes.
levelselect: ld a, 0 ld (movekey), a ld a, 0 out (0xfe), a ; clear screen ld hl, SCREEN ld (hl), 0 ld de, SCREEN + 1 ld bc, (256/8)*192+32*24 - 1 ldir ld de, map ld hl, levels ld bc, 16*12 ldir call drawminimap
At the start of the level select we clear the movekey to avoid any keys from being used from a previous state. Keydown is not cleared, so if the user is pressing a key, it won't get triggered before they release the key. We then clear the screen by writing a single zero to the first byte of the screen and then copy this over the rest of the screen using LDIR again, thus showing that LDIR is both memcpy and memset.
Then we "load" the level to the map using LDIR again, and call drawminimap before going into level selection loop. We'll look at the minimap thing in a moment.
levelselectloop: call scaninputs ld a, (movekey) bit 4, a jr z, levelselectloop ld a, 0 ld (movekey), a call findplayer ld (playerpos), hl call dirtymap mainloop:
We don't do much in the way of actually selecting a level yet, but we do call scaninputs again to see if the user has hit the mysterious 5th key, which is space, in which case we prepare the level for play by calling findplayer and dirtying the map, and falling through to the main loop.
We also make sure to clear the movekey so any depressed buttons don't get used right away in the game. One of these keys is also space which returns us to level select, so the game would just return right back here.
SCANKEY 7,0,4 ; space
To let the player move between the level select and game modes there's a new key, space, which needs to be checked in the scaninputs function. This is the same key for all input methods, so the check appears right after the keydata is updated. This also means that the SCANKEY macro had to move a bit, but remains unchanged.
moveplayer: ld a, (movekey) bit 4, a jp nz, levelselect
At the start of moveplayer we check for this and jump back to levelselect if space was pressed.
There's tons of code here, but it's all familiar, largely copy-paste from earlier code.
; Draw mini-map for the level selection drawminimap: ld hl, map + (16 * 12 - 1) ld bc, 0x100c ; 16x12 minimaploop: ld a, (hl) push hl push bc ld l, a ld h, 0 push hl ld hl, bc ld de, 0x0801 add hl, de ld bc, hl pop hl call drawminitile pop bc pop hl dec hl dec b jp nz, minimaploop ld b, 0x10 dec c jr nz, minimaploop ret
Drawminimap is actually just simplified version of our map drawing loop, with the dirty flag check removed. The draw offset is also changed so that the minimap, which is 1/4 of the actual, is drawn centered horizontally. To minidraw the minimap we minicall the drawminitile in our minimaploop.
; drawminitile, copies tile data to screen ; input: hl = tile, b = x, c = y ; destroys de, hl, bc, a drawminitile: ; Save these for later when we plot color push hl push bc ; One tile is 2*16 bytes add hl, hl ; *2 add hl, hl ; *4 add hl, hl ; *8 add hl, hl ; *16 add hl, hl ; *32 ld de, tiles add hl, de ld de, hl ; hl now is pointing at the start of tile x
Since we don't have separate minitiles yet, we just draw the first 8x8 of the tiles we do have. We'll revisit this function once we have the new assets for the minimap.
; Figuring out the screen coordinates is trickier; ; screen coordinate bits go like this: ; H | L ; 0 1 0 Y7 Y6 Y2 Y1 Y0 | Y5 Y4 Y3 X4 X3 X2 X1 X0 ; rotate the coordinate right three bits (to get to Y3, Y4, Y5 in L) ld a, c rrca rrca rrca ; AND any additional bits off and 0xe0 ; Add x add a, b ld l, a ; coordinate bottom byte done ; next we do the same for Y6 and Y7; no need to shift because we're in the ; right place. ld a, c and 0x18 ; AND extra bits off, and h is done. ld h, a ld bc, SCREEN add hl, bc ; now hl points at the screen offset we want
All of this should be familiar from the full-scale drawtile. All we've changed here is that instead of drawing 16x16 tiles we're drawing 8x8 ones, so we don't need to add the x and y coordinates twice.
; Instead of looping, we'll plot each pixel separately.. DUP 7 ld a, (de) ; Read pixels from data ld (hl), a ; Write to screen inc de ; Increment de and hl.. inc de inc h EDUP ld a, (de) ; Read pixels from data ld (hl), a ; Write to screen
And we only need to draw 8 bytes to get the bitmap to the screen, simplifying things here too. We still need to increment DE a couple of times since our source tile data is oversized.
; Bitmap done, color to do pop bc ld l, c ; y coordinate ld h, 0 ; we need to multiply y by 32; 32 colors per scanline add hl, hl ; x2 add hl, hl ; x4 add hl, hl ; x8 add hl, hl ; x16 add hl, hl ; x32 ld c, b ld b, 0 add hl, bc ; x offset ld bc, COLOR add hl, bc ld bc, hl ; bc now has color table offset
The color pointer setup is unchanged apart from the 16x16->8x8 change.
pop hl ; tile index ; One tile is 4 bytes of color add hl, hl ; x2 add hl, hl ; x4 ld de, tilecolors add hl, de ; hl now points at tile color ld de, hl ; de now points at tile ld hl, bc ; hl now points at screen
And of course the tile offset calculation is completely unchanged, making the actual data transfer part pretty small in comparison..
ld a, (de) ; read color ld (hl), a ; write color ret
Yup, that's it.
That takes care of the overall structure of the game. We still need the mini-tile assets, several levels and a way to pick between them. We also haven't dealt with game state yet, as simple as that is.
This chapter's version of the source as well as the keyselect.scr file is available here.
We're at 9197 bytes. Ick. Well, 6912 bytes of that is the image we included, but apart from that we grew by 488 bytes. That consists mostly of the duplicated map/tile draw functions as well as the empty map array. Still, we have 2/3 of the space we can use left, so we're doing fine.
Next up we'll keep filling that space with some new assets.
Any comments etc. can be emailed to me.