processor 6502 include "vcs.h" include "macro.h" include "xmacro.h" seg.u Variables org $80 ; In this example, we're going to tackle collision detection, ; which is one thing in the VCS that's easier than expected. ; The TIA has 15 different collision flags that can detect a ; collision between any of the 2 players, 2 missiles, ball, ; and playfield. You can check these flags at any time (at the ; end of the frame is pretty common). When you're done checking ; you clear them all at once by writing to CXCLR. ; For this example we'll use the ball object, and detect collisions ; between it and the playfield and the player. We only know ; the Y position of the ball and player (the X position is in ; the TIA chip) so we'll base our bounce decisions on the Y position ; of the ball (for playfield bounces) or the relative Y position of ; ball and player (for player bounces). ; Note: You can press the button to capture the ball. ; We're going to also include sound, which is generated by writing ; a volume register, a frequency register, and a mode register for ; one of two channels. counter byte ; increments each frame yplyr byte ; player y pos yball byte ; ball y pos animofs byte ; sprite data offset, used for animation 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 ; Color constants BGCOLOR equ $80 PLCOLOR equ $6c GNDCOLOR equ $c0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Enable ball if it is on this scanline (in X register) ; Modifies A. 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 lda #185-SpriteHeight sta yplyr ; player Y position, top to bottom ; Set player 0 horizontal position lda #80 ldx #0 jsr SetHorizPos2 ; Set ball horizontal position lda #84 ldx #4 jsr SetHorizPos2 sta WSYNC sta HMOVE ; Set ball initial velocity lda #1 sta yballvel lda #$30 sta xballvel ; 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 ldx #37 Underscan sta WSYNC dex bne Underscan ; Draw 192 scanlines ; First, 20 lines at top of screen ldx #0 ScanLoop1 sta WSYNC stx PF1 inx cpx #20 bne ScanLoop1 ; Top border of screen (8 lines) ScanLoop2 ; Fetch 1st part of bitmap data from table (we start on 20th line) lda #TopBorder0-20,x sta WSYNC sta PF0 DRAW_BALL ; draw the ball on this line? ; (only for collision purposes) ; Fetch 2nd and 3rd parts of bitmap lda #TopBorder1-20,x sta PF1 lda #TopBorder2-20,x sta PF2 inx cpx #28 bne ScanLoop2 ; Top half of screen (100 pixels) lda #0 sta PF0 sta PF1 sta PF2 ScanLoop3 sta WSYNC DRAW_BALL ; draw the ball on this line? inx cpx #128 bne ScanLoop3 ; Bottom half of screen with 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 tya clc ; clear carry flag adc animofs ; add animation offset (not for color though) tay lda Frame0,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 lda #GNDCOLOR ; set the background color sta COLUPF ScanLoop5 lda #$ff sta WSYNC 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 lda #SpriteHeight sta animofs ; use different bitmap when captured bne NoCollision ButtonNotPressed lda #0 sta captured ; clear capture flag sta animofs ; use regular bitmap ; 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 ; 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 ballCaptured ; Move ball vertically lda yball clc adc yballvel sta yball ; Move ball horizontally ; We use an fractional counter for the ball, and we have to ; set a different HMOVE value depending on if it's left or right lda xballvel bmi ballMoveLeft clc adc xballerr sta xballerr bcc ballCaptured lda #$f0 sta HMBL bne ballCaptured ballMoveLeft sec sbc xballerr sta xballerr bcs ballCaptured lda #$10 sta HMBL ballCaptured sta WSYNC sta HMOVE ; apply the move(s) sta HMCLR ; Joystick movement ; For up and down, we INC or DEC the Y Position lda #%00010000 ;Up? bit SWCHA bne SkipMoveUp ldx yplyr cpx #129 bcc SkipMoveUp dex stx yplyr lda captured ; captured? move the ball too beq SkipMoveUp dec yball SkipMoveUp lda #%00100000 ;Down? bit SWCHA bne SkipMoveDown 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 lda #%01000000 ;Left? bit SWCHA bne SkipMoveLeft ldx #$10 ;a 1 in the left nibble means go left SkipMoveLeft lda #%10000000 ;Right? bit SWCHA bne SkipMoveRight ldx #$F0 ;a -1 in the left nibble means go right... SkipMoveRight stx HMP0 ; set the move for player 0 lda captured ; captured? move the ball too beq NoCaptureMove stx HMBL ; set ball move register NoCaptureMove sta WSYNC sta HMOVE ; apply the move(s) ; 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 ; 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 RESP0,x ; fix coarse position sta HMP0,x ; set fine offset 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; ; Playfield top border bitmap TopBorder0 .byte #%11111111 .byte #%11111111 .byte #%11111111 .byte #%11111111 .byte #%11110000 .byte #%11000000 .byte #%10000000 .byte #%00000000 TopBorder1 .byte #%11111111 .byte #%11111111 .byte #%11111111 .byte #%11111111 .byte #%11111000 .byte #%11100000 .byte #%11000000 .byte #%10000000 TopBorder2 .byte #%11111111 .byte #%11111111 .byte #%11111111 .byte #%00000000 .byte #%00000000 .byte #%00000000 .byte #%00000000 .byte #%00000000 ; Epilogue org $fffc .word Start .word Start