From 633daddd6a45499e04090d5a4547442f1455f539 Mon Sep 17 00:00:00 2001 From: "Peter Helcmanovsky (Ped)" Date: Mon, 10 Feb 2020 21:45:59 +0100 Subject: [PATCH] Part 10 - game UI and helper functions (random, add-score, level-init, ...) Now most of the elements (to assemble simple "game" out of this) are in place, the only major item missing is scoring the "jump over snowball". In this part 10 we did add UI elements like score/bonus/lives, they are displayed in classic ULA layer, on "white" paper which is in palette set to be transparent, and layers priorities were modified to "USL", so these UI texts are above anything else (including sprites). We added also simple random generator from Baze, and added some debug features like random score increments to test new routines. And finally the code will switch to 3.5MHz at beginning, to give us some fun perspective on how much of frame time this little of machine code can take, and how difficult it was to produce more complex games on classic ZX Spectrum (where you would have to add all the manual drawing of sprites and background restoring). --- SpecBong.asm | 334 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 323 insertions(+), 11 deletions(-) diff --git a/SpecBong.asm b/SpecBong.asm index 46f46f4..b3f9a7e 100644 --- a/SpecBong.asm +++ b/SpecBong.asm @@ -12,6 +12,13 @@ ; (the upside down uncompressed 8bpp TGA has the advantage that it can be just binary ; included as L2 pixel data, from correct offset, no need of any further conversion) +; TODO plan to final release: +; - upon jump "drop" invisible sprite "egg" to calculate collisions against balls +; - track the hits of "egg" (only once per ball) and score it: first +100, next: 3x (300, 900, 2700, ...) +; - display "egg" bonus to player with delayed decay (needs probably two of these, but only single "egg" itself) +; - add logic: total score, total lives, bonus-timer +; - reset level, end of level, death of player, game over + ; adjusting sjasmplus syntax to my taste (a bit more strict than default) + enable Z80N OPT --syntax=abfw --zxnext @@ -58,15 +65,41 @@ start: ; disable interrupts, we will avoid using them to keep code simpler to understand di + ; setup bottom part of random seed by R + ld a,r + ld (Rand16.s),a + nextreg TURBO_CONTROL_NR_07,0 ; DEBUG - switch to 3.5MHz for fun + ; also to show how powerful the new HW features are, or in other way, + ; how little you can do in 3.5MHz and how optimized the classic games + ; must be to achieve anything more complex + ; make the Layer 2 visible and reset some registers (should be reset by NEXLOAD, but to be safe) nextreg DISPLAY_CONTROL_NR_69,$80 ; Layer 2 visible, ULA bank 5, Timex mode 0 - nextreg SPRITE_CONTROL_NR_15,$01 ; LoRes off, layer priority SLU, sprites visible + nextreg SPRITE_CONTROL_NR_15,%000'100'01 ; LoRes off, layer priority USL, sprites visible nextreg LAYER2_RAM_BANK_NR_12,9 ; visible Layer 2 starts at bank 9 nextreg LAYER2_CONTROL_NR_70,0 ; 256x192x8 Layer 2 mode, L2 palette offset +0 nextreg LAYER2_XOFFSET_NR_16,0 ; Layer 2 X,Y offset = [0,0] nextreg LAYER2_XOFFSET_MSB_NR_71,0 ; including the new NextReg 0x71 for cores 3.0.6+ nextreg LAYER2_YOFFSET_NR_17,0 + ; set all three clip windows (Sprites, Layer2, ULA) explicitly just to be sure + ; helps with bug in CSpect which draws sprites "over-border" even when it is OFF + nextreg CLIP_WINDOW_CONTROL_NR_1C,$03 ; reset write index to all three clip windows + nextreg CLIP_LAYER2_NR_18,0 + nextreg CLIP_LAYER2_NR_18,255 + nextreg CLIP_LAYER2_NR_18,0 + nextreg CLIP_LAYER2_NR_18,191 + nextreg CLIP_SPRITE_NR_19,0 + nextreg CLIP_SPRITE_NR_19,255 + nextreg CLIP_SPRITE_NR_19,0 + nextreg CLIP_SPRITE_NR_19,191 + nextreg CLIP_ULA_LORES_NR_1A,0 + nextreg CLIP_ULA_LORES_NR_1A,255 + nextreg CLIP_ULA_LORES_NR_1A,0 + nextreg CLIP_ULA_LORES_NR_1A,191 + + call InitUi ; will setup everything important about ULA screen, CLS + labels, etc. + ; setup Layer 2 palette - map palette data to $E000 region, to process them nextreg MMU7_E000_NR_57,$$BackGroundPalette ; map the memory with palette nextreg PALETTE_CONTROL_NR_43,%0'001'0'0'0'0 ; write to Layer 2 palette, select first palettes @@ -131,6 +164,10 @@ UploadSpritePatternsLoop: dec a jr nz,UploadSpritePatternsLoop ; do 64 patterns + ; setup high part of random seed by R + ld a,r + ld (Rand16.s+1),a + ; init SNOWBALLS_CNT snowballs and one player - the in-memory copy of sprite attributes ; init them at some debug positions, they just fly around mindlessly ld ix,SprSnowballs ; IX = address of first snowball sprite @@ -164,16 +201,8 @@ InitBallsLoop: ld (ix+S_SPRITE_4B_ATTR.mrx8),0 ld (ix+S_SPRITE_4B_ATTR.vpat),d - ; init player at debug position - ld ix,SprPlayer - ld (ix+S_SPRITE_4B_ATTR.x),32+16 ; near left of paper area - ld (ix+S_SPRITE_4B_ATTR.y),206 ; near bottom of paper area - ld (ix+S_SPRITE_4B_ATTR.mrx8),0 ; clear pal offset, mirrors, rotate, x8 - ld (ix+S_SPRITE_4B_ATTR.vpat),$80 + 2 ; pattern "2" (player - ld a,255 - ld (Player1SafeLandingY),a - ld hl,0 - ld (Player1LadderData),hl + ; init game state for new game + call GameStateInit_NewGame ; main loop of the game GameLoop: @@ -195,6 +224,12 @@ GameLoop: ; out 512 bytes in total (whole sprites buffer) otir otir + IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER + ; yellow border: to measure the UI code performance + ld a,6 + out (ULA_P_FE),a + ENDIF + call RefreshUi ; draws score, lives, jump-over-ball bonus scores, etc IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER ; magenda border: to measure the AI code performance ld a,3 @@ -217,6 +252,11 @@ GameLoop: ENDIF call SnowballvsPlayerCollision + ; DEBUG add some random value to score to verify it works (every 63 frames) + ld a,(TotalFrames) + and $3F + call z,DebugRandomScoreAdd + IF 0 ; DEBUG wait for fire key after frame .waitForFire: call ReadInputDevices : ld a,(Player1Controls) : bit JOY_BIT_FIRE,a : jr z,.waitForFire .waitForRelease: call ReadInputDevices : ld a,(Player1Controls) : bit JOY_BIT_FIRE,a : jr nz,.waitForRelease @@ -225,6 +265,225 @@ GameLoop: ; do the GameLoop infinitely jr GameLoop +DebugRandomScoreAdd: ; DEBUG + call Rand16 + ld a,l + xor h ; 0..255 to add to score (score is displayed as *100, so it's +0..+25,500) + call AddScore + call DecreaseBonus + ret + + ;------------------------------------------------------------------------------------- + ; Part 10 - UI drawing routines - using transparent ULA layer above everything + +GameStateInit_NewGame: + ; reset score to zero + ld hl,Player1Score + ld de,Player1Score+1 + ld (hl),'0' + ld bc,7 + ldir + ; reset lives + ld a,3 + ld (Player1Lives),a + ; | + ; fallthrough to GameStateInit_NewLevel + ; | + ; v +GameStateInit_NewLevel: + ; currently there's nothing special about new level + ; | + ; fallthrough to GameStateInit_NewLife + ; | + ; v +GameStateInit_NewLife: + ; reset bonus counter to 5000 (the last two are always zeroes, no need to re-init) + ld hl,"05" ; L = '5', H = '0' vs little-endian way of storing 16bit value + ld (LevelBonus),hl + ; reset Player position, and movement internals like ladder/jumping stuff + ld ix,SprPlayer + ld (ix+S_SPRITE_4B_ATTR.x),32+16 ; near left of paper area + ld (ix+S_SPRITE_4B_ATTR.y),206 ; near bottom of paper area + ld (ix+S_SPRITE_4B_ATTR.mrx8),0 ; clear pal offset, mirrors, rotate, x8 + ld (ix+S_SPRITE_4B_ATTR.vpat),$80 + 0 ; start with pattern 0 + xor a + ld (Player1Controls),a + ld (Player1ControlsCoolDown),a + ld (Player1LadderData+1),a ; just "tall" to zero is enough + ld (Player1JumpIdx),a + ld (Player1JumpDir),a + dec a ; A = 255 + ld (Player1SafeLandingY),a + ;TODO snowball AI reset + sprites reset + ret + +InitUi: + ; set ULA palette (to have background transparent) and do classic "CLS" + nextreg PALETTE_CONTROL_NR_43,%0'000'0'0'0'0 ; Classic ULA + custom palette + nextreg PALETTE_INDEX_NR_40,16+7 ; paper 7 + nextreg PALETTE_VALUE_NR_41,$E3 + nextreg PALETTE_INDEX_NR_40,16+8+7 ; paper 7 + bright 1 + nextreg PALETTE_VALUE_NR_41,$E3 + nextreg GLOBAL_TRANSPARENCY_NR_14,$E3 + nextreg TRANSPARENCY_FALLBACK_COL_NR_4A,%000'111'11 ; bright cyan as debug (shouldn't be seen) + ; do the "CLS" + ld hl,MEM_ZX_SCREEN_4000 + ld de,MEM_ZX_SCREEN_4000+1 + ld bc,MEM_ZX_ATTRIB_5800-MEM_ZX_SCREEN_4000 + ld (hl),l + ldir + ld (hl),P_WHITE|BLACK ; set all attributes to white paper + black ink + ld bc,32*24-1 + ldir + ; set attributes of some areas of screeen + ; "score" label attr + ld hl,MEM_ZX_ATTRIB_5800 + 0*32 + 24 + ld (hl),P_WHITE|WHITE|A_BRIGHT + ld c,8 + call .FillAttribs + ; score value attr + ld hl,MEM_ZX_ATTRIB_5800 + 1*32 + 24 + ld (hl),P_WHITE|YELLOW|A_BRIGHT + ld c,8 + call .FillAttribs + ; "bonus" label attr + ld hl,MEM_ZX_ATTRIB_5800 + 2*32 + 24 + ld (hl),P_WHITE|WHITE|A_BRIGHT + ld c,8 + call .FillAttribs + ; bonus value attr + ld hl,MEM_ZX_ATTRIB_5800 + 3*32 + 24 + ld (hl),P_WHITE|CYAN|A_BRIGHT + ld c,8 + call .FillAttribs + ; "lives" label attr + ld hl,MEM_ZX_ATTRIB_5800 + 4*32 + 24 + ld (hl),P_WHITE|WHITE|A_BRIGHT + ld c,8 + call .FillAttribs + ; print the static labels + ld de,MEM_ZX_SCREEN_4000 + 0*32 + 24 + ld hl,ScoreLabelTxt + ld b,8 + call PrintStringHlAtDe + ld de,MEM_ZX_SCREEN_4000 + 1*32 + 24 + ld hl,Player1Score + ld b,8 + call PrintStringHlAtDe + ld de,MEM_ZX_SCREEN_4000 + 2*32 + 24 + ld hl,BonusLabelTxt + ld b,8 + call PrintStringHlAtDe + ld de,MEM_ZX_SCREEN_4000 + 3*32 + 28 + ld hl,LevelBonus + ld b,4 + call PrintStringHlAtDe + ld de,MEM_ZX_SCREEN_4000 + 4*32 + 24 + ld hl,LivesLabelTxt + ld b,8 + call PrintStringHlAtDe + ret +.FillAttribs: + ld d,h + ld e,l + inc de + dec bc + ldir + ret + +RefreshUi: + ; refresh the score + ld de,MEM_ZX_SCREEN_4000 + 1*32 + 24 + ld hl,Player1Score + ld b,8 + call PrintStringHlAtDe + ; refresh the bonus score + ld de,MEM_ZX_SCREEN_4000 + 3*32 + 28 + ld hl,LevelBonus + ld b,4 + call PrintStringHlAtDe + ; refresh the lives UI (it's shown with sprites :) ) + ld ix,SprLivesUi + ld b,0 + ld a,(Player1Lives) + ld c,a ; amount of lives to show (others to hide) + ld hl,32+24*8+2 +.livesUiSetSpriteLoop: + ld (ix+S_SPRITE_4B_ATTR.mrx8),h ; no mirror/rotate flags + ld (ix+S_SPRITE_4B_ATTR.x),l + ld (ix+S_SPRITE_4B_ATTR.y),32+5*8+1 + ld e,32*2 ; will become pattern number 32 + ld a,b + cp c + rr e ; index < lives -> top bit (visible/hidden flag) + ld (ix+S_SPRITE_4B_ATTR.vpat),e + ld de,S_SPRITE_4B_ATTR + add ix,de + add hl,10 + inc b + cp 5 ; show at most 6 lives sprites + jr c,.livesUiSetSpriteLoop + ret + +AddScore: + ; In: A = score to add (0..255, score is automatically *100) + ld bc,(100<<8) | $FF ; B = 100, C = -1 + call .extractDigit + push bc + ld bc,(10<<8) | $FF ; B = 10, C = -1 + call .extractDigit + ; C = tens, A = ones (C on stack = hundreds) + ; add the digits to the string in memory representing score + ld hl,Player1Score+5 ; start at third digit from right ("00" is fixed) + call .addDigit + ld a,c + pop bc + ld b,5 +.updateDigitsLoop: + dec hl + call .addDigit + ld a,c + ld c,0 + djnz .updateDigitsLoop + ret +.addDigit: + ; A = current digit amount 0..10, C = next digit amount 0..9 (!) + add a,(hl) + cp '0'+10 + ld (hl),a ; digit updated, check if carry has to happen + ret c ; '0'..'9' = ok, done + sub 10 ; beyond '9' -> fix char and increment next digit + ld (hl),a + inc c + ret +.extractDigit: + inc c + sub b + jr nc,.extractDigit + add a,b + ret + +DecreaseBonus: + ; decrement hundreds digit + ld hl,(LevelBonus) ; L = first digit char, H = second digit char + dec h + ld a,'0'-1 + cp h + jr c,.writeNewValue + ; hundreds digit was '0' before, wrap around or refuse to decrement when " 000" + dec l + cp l + ret nc ; value was already " 000", can decrement more, ignore + inc a + cp l + jr nz,.keepFirstDigit + ld l,' ' ; exchange first '0' with space +.keepFirstDigit: + ld h,'9' ; L is still valid 0..9, fix the hundreds digit to "9" +.writeNewValue: + ld (LevelBonus),hl + ret + ;------------------------------------------------------------------------------------- ; the collision detection player vs snowball (only player vs balls) @@ -729,6 +988,22 @@ SnowballsAI: ;------------------------------------------------------------------------------------- ; utility subroutines +PrintStringHlAtDe: + ; In: HL = string address, DE = ULA VRAM address, B = length of string + ; modifies: HL,DE,B + push hl + ld l,(hl) + ; do HL = MEM_ROM_CHARS_3C00 + char_value*8 + add hl,hl ; regular ASCII should fit into L only + ld h,(high MEM_ROM_CHARS_3C00)/4 + .2 add hl,hl ; remaining 4x to get final address of font data + .8 ldws ; 8x copy font data to ULA VRAM + add de,1-$800 ; adjust DE to beginning of next char + pop hl ; HL to string pointer and advance + inc hl + djnz PrintStringHlAtDe + ret + ReadInputDevices: ; read Kempston port first, will also clear the inputs in a,(KEMPSTON_JOY1_P_1F) @@ -799,6 +1074,28 @@ WaitForScanlineUnderUla: ; I don't need to read MSB. 256+192 = 448 -> such scanline is not part of any mode. ret +; From http://map.grauw.nl/sources/external/z80bits.html#4.2 (Milos "baze" Bazelides Z80 bits) +Rand16: + ; Out: HL = pseudo-random number, period 65536 + ; modifies: A, DE +.s=$+1 ld de,0 ; seed + ld a,d + ld h,e + ld l,253 + or a + sbc hl,de + sbc a,0 + sbc hl,de + ld d,0 + sbc a,d + ld e,a + sbc hl,de + jr nc,.storeSeed + inc hl +.storeSeed: + ld (.s),hl + ret + ;------------------------------------------------------------------------------------- ; data area @@ -806,6 +1103,8 @@ TotalFrames: ; count frames for purposes of slower animations/etc DD 0 CollisionFxCount: ; count the dynamically created extra FX sprites displaying collision DB 0 +LevelBonus: + DB "xx00" ; bits encoding inputs as Kempston/MD: https://wiki.specnext.dev/Kempston_Joystick Player1Controls: @@ -821,6 +1120,18 @@ Player1SafeLandingY: Player1LadderData: DB 0, 0 ; topY, tall (zero tall when not at ladder) +Player1Score: ; max score 99,999,900 (8 characters in ULA mode, last two "00" always :)) + DS 8, 'x' +Player1Lives: + DB 0 + +ScoreLabelTxt: + DB " SCORE: " +BonusLabelTxt: + DB " BONUS: " +LivesLabelTxt: + DB " LIVES: " + ; ladders (256x192 positions in BMP): ; [152,163,185], [40,134,156], [152,105,127], [40,75,98], [152,46,68], [96, 19, 43] LaddersData: ; adjustments: "32+" for sprite coordinates, "-8" and "-16" to adjust by player size @@ -858,6 +1169,7 @@ SNOWBALLS_CNT EQU 28 SprSnowballs: EQU Sprites + 0*S_SPRITE_4B_ATTR ; first snowball sprite at this address SprPlayer: EQU Sprites + SNOWBALLS_CNT*S_SPRITE_4B_ATTR ; player sprite is here SprCollisionFx: EQU SprPlayer + S_SPRITE_4B_ATTR +SprLivesUi: EQU SprCollisionFx + SNOWBALLS_CNT * S_SPRITE_4B_ATTR ; platforms collisions - data of platforms and their heights + extras