processor 6502 include "vcs.h" include "macro.h" include "xmacro.h" ; We've got collisions working, but now we'd like some more ; interaction. We can make a little "breakout" style game ; where the ball knocks out rows of bricks. We'll need to ; draw several rows of bricks, any or all of which might be ; missing. ; We'll use a technique called "asychronous playfields". ; Remember that the playfield is either symmetric (20 pixels ; followed by the same 20 pixels reversed) or repeated (20 pixels ; repeated twice). But if we change the playfield registers ; *during* the scanline, we can make the second half a ; different bitmap than the first half. ; We're going to move away from the HMPx/HMOVE method of ; setting object position and use the SetHorizPos method, since ; we really need to know the X position of both player and ball ; at all times. The way the subroutine is written, this takes ; two scanlines per object. But we do it during the overscan ; period at the end of the frame, and we've got the cycles ; to spare. ; Also, we're going to keep score and have a rudimentary ; scoreboard, which makes this sort of an actual game! ; Fun fact: Messing with the HMOVE register causes a "comb" ; effect on the left 8 pixels of the screen, which can be seen ; at the bottom of the screen in the overscan region. seg.u Variables org $80 XPlyr byte ; player x pos YPlyr byte ; player y pos XBall byte ; ball x pos YBall byte ; ball y pos SpritePtr word ; sprite pointer YSprOfs byte ; temp sprite offset YBallVel byte ; ball Y velocity XBallVel byte ; ball X velocity XBallErr byte ; ball X fractional error Captured byte ; ball capture flag AVol0 byte ; shadow register for AVOL0 Score byte ; current BCD-encoded score Temp byte ; temporary storage Bricks ds 36 ; brick bitmap (6x6 bytes) ScoreHeight equ 20 ; height of top scoreboard BrickYStart equ 32 ; starting Y coordinate of bricks BrickHeight equ 16 ; height of each brick in pixels NBrickRows equ 6 ; number of lines of bricks NBL equ NBrickRows ; abbreviation for number of brick rows BytesPerRow equ 6 ; number of bytes for each row of bricks BricksPerRow equ 40 ; number of bricks in each row ; (two bytes have only 4 active pixels) ; Color constants BGCOLOR equ #$80 PLCOLOR equ #$6c GNDCOLOR equ #$c0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Enable ball if it is on this scanline (in X register) ; Modifies A. ; Takes 13 cycles if ball is present, 12 if absent. MAC DRAW_BALL lda #%00000000 cpx YBall bne .noball lda #%00000010 ; for ENAM0 the 2nd bit is enable .noball sta ENABL ; enable ball ENDM ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; seg Code org $f000 ; Initialize and set initial offsets of objects. Start CLEAN_START ; Set player 0 vertical position lda #185-SpriteHeight sta YPlyr ; player Y position, top to bottom ; Set player 0 horizontal position lda #70 sta XPlyr ldx #0 jsr SetHorizPos2 ; Set ball horizontal position lda #0 sta XBall ldx #4 jsr SetHorizPos2 sta WSYNC sta HMOVE ; Set ball initial velocity lda #1 sta YBallVel lda #129 sta YBall lda #$40 sta XBallVel ; Set up initial bricks ldx #0 lda #$ff SetupBricks ;txa ; uncomment for a sparse brick pattern sta Bricks,x inx cpx #BytesPerRow*NBrickRows bne SetupBricks ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Next frame loop NextFrame VERTICAL_SYNC ; in case the ball is on screen lda ColorFrame0 ; load 1st entry of color data sta COLUP0 ; set sprite 0 color ; Set up playfield lda #BGCOLOR ; set the background color sta COLUBK lda #PLCOLOR ; set the playfield color sta COLUPF lda #%00010101 ; playfield reflection and ball size/priority sta CTRLPF lda #0 ; blank out the playfield sta PF0 sta PF1 sta PF2 ; 37 lines of VBLANK TIMER_SETUP 37 ; Set up sprite pointer depending on button press lda #Frame0 sta SpritePtr+1 ; normal sprite bitmap lda INPT4 ;read button input bmi ButtonNotPressed2 ;skip if button not pressed lda #Frame1 sta SpritePtr+1 ; alternate sprite bitmap ButtonNotPressed2 TIMER_WAIT ; Draw 192 scanlines. ldx #0 ; X will contain the frame Y coordinate ; First, we'll draw the scoreboard. ; Put the playfield into score mode (bit 2) which gives ; two different colors for the left/right side of ; the playfield (given by COLUP0 and COLUP1). lda #%00010010 ; score mode + 2 pixel ball sta CTRLPF lda #$48 sta COLUP0 ; set color for left lda #$a8 sta COLUP1 ; set color for right ; We need to extract each digit of the BCD-coded score ; (there are two digits, each 4 bits) and find the appropriate ; entry in the DigitsBitmap bitmap table. ; We'll just draw one digit to keep it simple. ScanLoop1a clc ; clear carry flag ; Digits are 5 pixels high, so we need to multiply each ; digit by 5 to find our digit in the bitmap table. lda Score ; grab the BCD score and #$0F ; mask out the least significant digit sta Temp asl asl adc Temp sta Temp ; tmp = score * 5 ; Now we divide our current Y coordinate by 2 ; to get the index into the digit bitmap. txa ror ; A = Ycoord / 2 adc Temp ; A += tmp tay lda DigitsBitmap,y ; A = DigitsBitmap[offset] and #$0F ; mask out the rightmost digit sta WSYNC sta PF1 ; store digit to playfield 1 register DRAW_BALL ; draw the ball on this line? ; (only for collision purposes) inx cpx #10 ; digits are 5 pixels high * 2 lines per pixel bcc ScanLoop1a ; Clear the playfield lda #0 sta PF1 ; Turn playfield reflection off, since our brick field ; will be drawn asymetrically (and turn score mode off) lda #%00010100 ; no reflection + ball priority + 2 pixel ball sta CTRLPF ; Continue until the bricks start on line 32. ScanLoop1b sta WSYNC DRAW_BALL ; draw the ball on this line? ; (only for collision purposes) inx cpx #BrickYStart bne ScanLoop1b ; Next we'll draw the brick field, which is asymmetrical. ; We use two loops: the inner loop draws a row of bricks ; and the outer loop sets up for the next row. ; Timing is very important here! Note that we skip ; drawing the ball if it falls on a line after we start a ; new row. This will cause a little flicker but it is not ; very noticable. SLEEP 44 ; make sure we start near the end of scanline ldy #$ff ; start with row = -1 ScanLoop3b iny ; go to next brick row lda #BrickHeight ; for the outer loop, we count sta Temp ; 'brickheight' scan lines for each row cpy #NBrickRows ; done drawing all brick rows? bcc ScanSkipSync ; no -- but don't have time to draw ball! jmp DoneBrickDraw ; exit outer loop ScanLoop3a ; These instructions are skipped on lines after the brick row changes. ; We need the extra cycles. DRAW_BALL ; draw the ball on this line? ScanSkipSync sta WSYNC < stx COLUPF ; change colors for bricks ; Load the first byte of bricks ; Bricks are stored in six contiguous arrays (row-major) lda Bricks+NBL*0,y sta PF0 ; store first playfield byte ; Store the next two bytes lda Bricks+NBL*1,y sta PF1 lda Bricks+NBL*2,y sta PF2 ; Here's the asymmetric part -- by this time the TIA clock ; is far enough that we can rewrite the same PF registers ; and display new data on the right side of the screen inx ; good place for INX b/c of timing nop ; yet more timing lda Bricks+NBL*3,y sta PF0 lda Bricks+NBL*4,y sta PF1 lda Bricks+NBL*5,y sta PF2 dec Temp beq ScanLoop3b ; all lines in current brick row done? bne ScanLoop3a ; branch always taken ; Clear playfield from bricks loop DoneBrickDraw SLEEP 6 lda #0 sta PF0 sta PF1 sta PF2 ; Draw bottom half of screen with player sprite. ; Setup 'ysprofs' which is the calculated offset into ; sprite lookup tables (it can exceed bounds, we'll test) ; Since the sprite table is reversed, the starting offset is ; Yplyr - Ystart - SpriteHeight lda YPlyr sec sbc #128-SpriteHeight sta YSprOfs ScanLoop4 ; Is this scanline within sprite bounds? dec YSprOfs lda YSprOfs cmp #SpriteHeight ; sprite is 16 pixels high + padding bcc InSprite lda #0 ; no sprite, draw the padding InSprite tay lda ColorFrame0,y ; load color data pha ; push color data onto stack lda (SpritePtr),y ; load bitmap data sta WSYNC ; wait for next scanline (as late as possible!) sta GRP0 ; set sprite 0 pixels pla ; pull bitmap data from stack sta COLUP0 ; set sprite 0 color DRAW_BALL ; draw the ball on this line? inx cpx #184 bne ScanLoop4 ; repeat next scanline until finished ; 8 more pixels for bottom border, and then we'll just leave it ; on for the overscan region. ldy #$c8 ; set the playfield color ScanLoop5 dey ; make a nice gradient lda #$ff sta WSYNC sty COLUPF ; set the playfield color sta PF0 sta PF1 sta PF2 lda #0 sta GRP0 inx cpx #192 bne ScanLoop5 ; Disable ball lda #0 sta ENABL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; 30 lines of overscan needed, but we have lots of logic to do. ; So we're going to use the PIA timer to let us know when ; almost 30 lines of overscan have passed. ; This handy macro does a WSYNC and then sets up the timer. TIMER_SETUP 30 ; Check for collisions lda #%01000000 bit CXP0FB ; collision between player 0 and ball? bne PlayerCollision lda #%10000000 bit CXBLPF ; collision between playfield and ball? bne PlayfieldCollision beq NoCollision ; Now we bounce the ball depending on where it is PlayerCollision ; Is the button pressed? if so, just capture the ball lda INPT4 ;read button input bmi ButtonNotPressed ;skip if button not pressed inc Captured ; set capture flag bne NoCollision ButtonNotPressed lda #0 sta Captured ; clear capture flag ; See if we bounce off of top half or bottom half of player ; (yplyr + height/2 - yball) ldx #1 lda YPlyr clc adc #SpriteHeight/2 sec sbc YBall bmi StoreVel ; bottom half, bounce down ldx #$ff ; top half, bounce up bne StoreVel PlayfieldCollision ; Which brick do we break? ; try the one nearest to us lda YBall ldx XBall jsr BreakBrick bmi CollisionNoBrick ; return -1 = no brick found ; Did we hit the top or the bottom of a brick? ; If top, bounce up, otherwise down ldx #$ff ; ball velocity = up cmp #BrickHeight/2 ; top half of brick? bcc BounceBallUp ; yofs < brickheight/2 ldx #1 ; ball velocity = down BounceBallUp ; Go to BCD mode and increment the score. ; This treats 'score' as two decimal digits, ; one in each nibble, for ADC and SBC operations. sed lda Score clc adc #1 sta Score cld jmp StoreVel CollisionNoBrick ; If bouncing off top of playfield, bounce down ldx #1 lda YBall bpl StoreVel ; Otherwise bounce up ldx #$ff StoreVel ; Store final velocity stx YBallVel ; Make a little sound txa adc #45 sta AUDF0 ; frequency lda #6 sta AVol0 ; shadow register for volume NoCollision ; Clear collision registers for next frame sta CXCLR ; Ball captured? if so, no motion lda Captured bne DoneMovement ; Move ball vertically lda YBall clc adc YBallVel bne NoBallHitTop ldx #1 stx YBallVel NoBallHitTop sta YBall ; Move ball horizontally lda XBallVel ; signed X velocity bmi BallMoveLeft ; < 0? move left clc adc XBallErr sta XBallErr ; XBallErr += XBallVel bcc DoneMovement ; no wrap around? done inc XBall ; XBall += 1 lda XBall cmp #160 ; moved off right side? bcc DoneMovement ; no, done lda #0 sta XBall ; wrap around to left beq DoneMovement ; always taken BallMoveLeft clc adc XBallErr sta XBallErr bcs DoneMovement dec XBall ; decrement xball lda XBall cmp #160 bcc DoneMovement lda #159 sta XBall DoneMovement ; Joystick player movement ; For up and down, we INC or DEC the Y Position lda #%00010000 ;Up? bit SWCHA bne SkipMoveUp ; bit set? skip move ldx YPlyr cpx #129 bcc SkipMoveUp dex stx YPlyr lda Captured ; captured? move the ball too beq SkipMoveUp dec YPlyr SkipMoveUp lda #%00100000 ;Down? bit SWCHA bne SkipMoveDown ; bit set? skip move ldx YPlyr cpx #185-SpriteHeight bcs SkipMoveDown inx stx YPlyr lda Captured ; captured? move the ball too beq SkipMoveDown inc YBall SkipMoveDown ; Note that the horizontal position is not contained in RAM, ; but inaccessibly inside the TIA's registers! Some games can ; get away with this if they use the collision registers. ldx #0 ; assume speed is 0 if no movement ; We'll test the left/right flags using a special feature of ; the BIT instruction, which sets the N and V flags to the ; 7th and 6th bit of the target. bit SWCHA bvs SkipMoveLeft ; V flag set? lda XPlyr beq SkipMoveLeft ; don't allow move left of screen dec XPlyr lda Captured beq SkipMoveLeft dec XBall ; if captured, also move the ball SkipMoveLeft bit SWCHA bmi SkipMoveRight ; N flag set? lda XPlyr cmp #150 bcs SkipMoveRight ; don't allow move right of screen inc XPlyr lda Captured beq SkipMoveRight inc XBall ; if captured, also move the ball SkipMoveRight ; Set ball position using SetHorizPos lda XBall ldx #4 jsr SetHorizPos2 sta WSYNC sta HMOVE ; Set player position using SetHorizPos lda XPlyr ldx #0 jsr SetHorizPos2 sta WSYNC sta HMOVE ; Play audio from shadow register? ldx AVol0 beq NoAudio dex ; decrement volume every frame stx AUDV0 ; store in volume hardware register stx AVol0 ; store in shadow register lda #3 sta AUDC0 ; shift counter mode 3 for weird bounce sound NoAudio ; Wait until our timer expires and then WSYNC, so then we'll have ; passed 30 scanlines. This handy macro does this. TIMER_WAIT ; Goto next frame jmp NextFrame ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Subroutine to try to break a brick at a given X-Y coordinate. ; X contains the X coordinate. ; A contains the Y coordinate. ; On return, A = -1 if no brick was present, ; otherwise A = Y offset (0-brickheight-1) of brick hit. BreakBrick ldy #$ff sec sbc #BrickYStart ; subtract top Y of brick field ; Divide by brick height DivideRowLoop iny sbc #BrickHeight bcs DivideRowLoop ; loop until < 0 cpy #NBrickRows bcs NoBrickFound ; Now that we have the line, get byte and bit offset for brick clc adc #BrickHeight pha ; save the remainder to return as result txa clc adc #3 ; adjust because SetHorizPos is off by a few pixels lsr lsr ; divide X coordinate by 4 tax ; transfer brick column to X tya ; load brick row # in A clc adc PFOfsTable,x ; add offset tay lda PFMaskTable,x eor #$ff and Bricks,y cmp Bricks,y ; was there a change? beq NoBrickFound2 ; no, so return -1 as result sta Bricks,y pla ; return remainder as result rts NoBrickFound2 pla ; pull the remainder, but ignore it NoBrickFound lda #$FF ; return -1 as result rts ; SetHorizPos2 - Sets the horizontal position of an object. ; The X register contains the index of the desired object: ; X=0: player 0 ; X=1: player 1 ; X=2: missile 0 ; X=3: missile 1 ; X=4: ball ; This routine does a WSYNC both before and after, followed by ; a HMOVE and HMCLR. So it takes two scanlines to complete. SetHorizPos2 sta WSYNC ; start a new line sec ; set carry flag DivideLoop sbc #15 ; subtract 15 bcs DivideLoop ; branch until negative eor #7 ; calculate fine offset asl asl asl asl sta HMP0,x ; set fine offset sta RESP0,x ; fix coarse position sta WSYNC sta HMOVE ; apply the previous fine position(s) sta HMCLR ; reset the old horizontal position(s) rts ; return to caller ; Height of our sprite in lines SpriteHeight equ 17 ; Bitmap data "standing" position Frame0 .byte #0 .byte #%01101100;$F6 .byte #%00101000;$86 .byte #%00101000;$86 .byte #%00111000;$86 .byte #%10111010;$C2 .byte #%10111010;$C2 .byte #%01111100;$C2 .byte #%00111000;$C2 .byte #%00111000;$16 .byte #%01000100;$16 .byte #%01111100;$16 .byte #%01111100;$18 .byte #%01010100;$18 .byte #%01111100;$18 .byte #%11111110;$F2 .byte #%00111000;$F4 ; Bitmap data "throwing" position Frame1 .byte #0 .byte #%01101100;$F6 .byte #%01000100;$86 .byte #%00101000;$86 .byte #%00111000;$86 .byte #%10111010;$C2 .byte #%10111101;$C2 .byte #%01111101;$C2 .byte #%00111001;$C2 .byte #%00111000;$16 .byte #%01101100;$16 .byte #%01111100;$16 .byte #%01111100;$18 .byte #%01010100;$18 .byte #%01111100;$18 .byte #%11111110;$F2 .byte #%00111000;$F4 ; Color data for each line of sprite ColorFrame0 .byte #$FF ; ball color if not sharing line with player sprite .byte #$F6; .byte #$86; .byte #$86; .byte #$86; .byte #$C2; .byte #$C2; .byte #$C2; .byte #$C2; .byte #$16; .byte #$16; .byte #$16; .byte #$18; .byte #$18; .byte #$18; .byte #$F2; .byte #$F4; ; Bitmap pattern for digits DigitsBitmap .byte $0E ; | XXX | $F5C5 Leading zero is not drawn .byte $0A ; | X X | $F5C6 because it's never used. .byte $0A ; | X X | $F5C7 .byte $0A ; | X X | $F5C8 .byte $0E ; | XXX | $F5C9 .byte $22 ; | X X | $F5CA .byte $22 ; | X X | $F5CB .byte $22 ; | X X | $F5CC .byte $22 ; | X X | $F5CD .byte $22 ; | X X | $F5CE .byte $EE ; |XXX XXX | $F5CF .byte $22 ; | X X | $F5D0 .byte $EE ; |XXX XXX | $F5D1 .byte $88 ; |X X | $F5D2 .byte $EE ; |XXX XXX | $F5D3 .byte $EE ; |XXX XXX | $F5D4 .byte $22 ; | X X | $F5D5 .byte $66 ; | XX XX | $F5D6 .byte $22 ; | X X | $F5D7 .byte $EE ; |XXX XXX | $F5D8 .byte $AA ; |X X X X | $F5D9 .byte $AA ; |X X X X | $F5DA .byte $EE ; |XXX XXX | $F5DB .byte $22 ; | X X | $F5DC .byte $22 ; | X X | $F5DD .byte $EE ; |XXX XXX | $F5DE .byte $88 ; |X X | $F5DF .byte $EE ; |XXX XXX | $F5E0 .byte $22 ; | X X | $F5E1 .byte $EE ; |XXX XXX | $F5E2 .byte $EE ; |XXX XXX | $F5E3 .byte $88 ; |X X | $F5E4 .byte $EE ; |XXX XXX | $F5E5 .byte $AA ; |X X X X | $F5E6 .byte $EE ; |XXX XXX | $F5E7 .byte $EE ; |XXX XXX | $F5E8 .byte $22 ; | X X | $F5E9 .byte $22 ; | X X | $F5EA .byte $22 ; | X X | $F5EB .byte $22 ; | X X | $F5EC .byte $EE ; |XXX XXX | $F5ED .byte $AA ; |X X X X | $F5EE .byte $EE ; |XXX XXX | $F5EF .byte $AA ; |X X X X | $F5F0 .byte $EE ; |XXX XXX | $F5F1 .byte $EE ; |XXX XXX | $F5F2 .byte $AA ; |X X X X | $F5F3 .byte $EE ; |XXX XXX | $F5F4 .byte $22 ; | X X | $F5F5 .byte $EE ; |XXX XXX | $F5F6 ; Playfield bitmasks for all 40 brick columns PFMaskTable REPEAT 2 .byte #$10,#$20,#$40,#$80 .byte #$80,#$40,#$20,#$10,#$08,#$04,#$02,#$01 .byte #$01,#$02,#$04,#$08,#$10,#$20,#$40,#$80 REPEND ; Brick array byte offsets for all 40 brick columns PFOfsTable .byte NBL*0,NBL*0,NBL*0,NBL*0 .byte NBL*1,NBL*1,NBL*1,NBL*1, NBL*1,NBL*1,NBL*1,NBL*1 .byte NBL*2,NBL*2,NBL*2,NBL*2, NBL*2,NBL*2,NBL*2,NBL*2 .byte NBL*3,NBL*3,NBL*3,NBL*3 .byte NBL*4,NBL*4,NBL*4,NBL*4, NBL*4,NBL*4,NBL*4,NBL*4 .byte NBL*5,NBL*5,NBL*5,NBL*5, NBL*5,NBL*5,NBL*5,NBL*5 ; Epilogue org $fffc .word Start .word Start