8bitworkshop/presets/examples/brickgame.a

738 lines
20 KiB
Plaintext

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
lda #>Frame0
sta SpritePtr+1 ; normal sprite bitmap
lda INPT4 ;read button input
bmi ButtonNotPressed2 ;skip if button not pressed
lda #<Frame1
sta SpritePtr
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