processor 6502 include "vcs.h" include "macro.h" include "xmacro.h" ; For lots of games, we'd like to display more than two sprites. ; There are lots of different ways to tackle this on the VCS, ; but we're going to try for a generalized approach that lets ; use have N different sprites at any X-Y coordinate, each with ; its own bitmap and color table. This is tricky because we can ; only do so much on each scanline. ; Our approach is to separate the problem into three phases. ; In the sort phase, we sort all sprites by Y coordinate. ; We do one sort pass per frame, so it may take several frames ; for the sort to stabilize. ; In the positioning phase, we look at the sprites in Y-sorted ; order, looking several lines ahead to see if a sprite is ; coming up. We then allocate it to one of the two player ; objects in hardware and set its position using the SetHorizPos ; method. We can set one or both of the player objects this way. ; In the display phase, we display the objects which we previously ; assigned and positioned. First we figure out how many scanlines are ; required. If only one object is scheduled, we just use its height. ; If two objects are scheduled, we go until the bottommost line has ; been displayed. We then loop through, fetching pixels and colors ; for one or both objects (up to four lookup tables) and setting ; registers at the appropriate time. We don't have time to do much ; else, so we don't look for any new objects to schedule until ; we're done with this loop. ; This scheme can only display up to two objects on a given ; scanline, so if the system tries to schedule a third, it will ; be ignored. Also, the positioning routine takes a few scanlines ; to complete, so if the top of a sprite is too close to the ; bottom of another sprite, the latter may not be displayed. ; ; To mitigate this, we increment a priority counter when a ; sprite entry is missed. In the sort phase, we move those sprites ; ahead of lower priority sprites in the sort order. This makes ; overlapping sprites flicker instead of randomly disappear. ; ; Note that we sprinkle NEWLINE macros around the codebase. This ; macro increments the 'scanline' variable and then does a WSYNC ; to sync to the next line. These locations are carefully selected ; so that we don't run out of CPU time on any line. ; (If you open the Console on your browser, you'll see debug ; statements printed when this happens) ; ; There are still some timing issues to fix as you'll see when you ; move the adventure person around with the joystick. These might ; add additional lines to the display. seg.u Variables org $80 Scanline byte ; scanline to draw next CurIndex byte ; current sprite # to try to schedule PData0 word ; pointer (lo/hi) to player 0 bitmap data PColr0 word ; pointer to player 0 color data PData1 word ; pointer to player 1 bitmap data PColr1 word ; pointer to player 1 color data SIndx0 byte ; next y-position to draw player 0 ; or during draw, index into sprite ; zero means not assigned SIndx1 byte ; ... for player 1 SSize0 byte ; sprite size for player 0 SSize1 byte ; sprite size for player 1 NSprites equ 8 ; max # of sprites XPos0 ds NSprites ; x coord for each sprite YPos0 ds NSprites ; y coord for each sprite Sorted0 ds NSprites ; sorted list of sprite indices Priority0 ds NSprites ; sprite priority list, if missed ; Macro to go to the next line so we don't forget ; to increment 'scanline'. MAC NEWLINE inc Scanline sta WSYNC ENDM ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; seg Code org $f000 ; Initialize and set initial X and Y offsets of objects. Start CLEAN_START ldx #0 lda #10 ldy #40 InitLoop sty XPos0,x sta YPos0,x clc adc #20 iny iny iny iny inx cpx #NSprites bne InitLoop ; Initialize initial sort order ldx #0 InitLoop2 txa sta Sorted0,x inx cpx #NSprites bne InitLoop2 ; Next frame loop NextFrame ; VSYNC and VBLANK periods VERTICAL_SYNC TIMER_SETUP 37 ; Do joystick movement jsr MoveJoystick ; Do one iteration of bubble sort on sprite indices ldx #NSprites-2 SortLoop jsr SwapSprites dex bpl SortLoop ; loop until <= 0 ; Reset scanline counter and sprite objects ldx #0 stx Scanline stx CurIndex stx SIndx0 stx SIndx1 stx SSize0 stx SSize1 TIMER_WAIT ; end of VBLANK ; Scanline loop ; Start with WSYNC so we are at a known state lda #$60 NEWLINE sta COLUBK ; set the background color NextFindSprite ; Schedule a player to a sprite jsr FindAnotherSprite ; See if time to draw jsr DrawSpritesIfTime ; repeat until all scanlines drawn lda Scanline cmp #192 bcc NextFindSprite ; end of Scanline loop NoMoreScanlines ; Clear all colors to black before overscan ldx #0 stx COLUBK stx COLUP0 stx COLUP1 stx COLUPF ; 30 lines of overscan TIMER_SETUP 30 TIMER_WAIT ; Go to next frame jmp NextFrame ; We were too late to display a sprite. ; Put it earlier in the sort order and try next frame. ; X = sort index .MissedSprite subroutine ; Have we already looked at all the sprites? cpx #NSprites bcs .OutOfSprites ; Increment priority for this sort entry inc Priority0,x ; Go to next sort index, until we get to the end inx stx CurIndex .OutOfSprites NEWLINE rts ; Try to assign the next sprite in the sort order into ; one of the two player slots. ; Uses 1 line (if no sprite found) or 3 lines (if sprite found) FindAnotherSprite ; subroutine entry point ldx CurIndex ldy Sorted0,x ; get sprite index # in Y-sorted order lda YPos0,y ; get Y position of sprite sec sbc Scanline ; SpriteY - Scanline ; Don't schedule the sprite if it's too soon or its scanline ; has already passed -- mark it missed bmi .MissedSprite ; passed it? (or > 127 lines away) cmp #3 bcc .MissedSprite ; less than 3 scanlines away .SpriteUpcoming ; A sprite is starting soon, now we need to schedule it ; to either one of the player objects lda XPos0,y ; Is player 1 available? ldx SIndx1 bne .Plyr1NotReady ; Due to timing issues, we have artifacts if player 1 is ; too close to the left edge of the screen. So we'd prefer to ; put those sprites in the player 0 slot. cmp #34 ; X < 34 bcc .Plyr1NotReady ; First let's set its horizontal offset (requires 2 lines) ldx #1 jsr SetHorizPos ; set horizontal position (does WSYNC) ; Assign the sprite's Y position to player 1 lda YPos0,y sta SIndx1 ; Get index into SpriteDataMap (index * 4) lda MultBy4,y tay ; Copy addresses of pixel/color maps to player 1 lda SpriteDataMap,y sta PData1 lda SpriteDataMap+1,y sta PData1+1 lda SpriteDataMap+2,y sta PColr1 lda SpriteDataMap+3,y sta PColr1+1 ; Get the sprite height as the first byte of the color map ldy #0 lda (PColr1),y sta SSize1 jmp .SetupDone .Plyr1NotReady ldx SIndx0 bne .NoNearSprite ; both players in use ; Player 0 is available ; This is essentially the same as the player 1 routine ldx #0 jsr SetHorizPos lda YPos0,y sta SIndx0 lda MultBy4,y tay lda SpriteDataMap,y sta PData0 lda SpriteDataMap+1,y sta PData0+1 lda SpriteDataMap+2,y sta PColr0 lda SpriteDataMap+3,y sta PColr0+1 ldy #0 lda (PColr0),y sta SSize0 .SetupDone inc CurIndex ; go to next sprite in sort order .NoNearSprite NEWLINE sta HMOVE ; apply the previous fine position(s) sta HMCLR ; reset the old horizontal position(s) rts ; Draw sprites if they are starting in the next few scanlines. DrawSpritesIfTime subroutine ; See if either sprite is almost time to draw (within 4 lines) lda SIndx0 sec sbc Scanline cmp #4 bcc .DrawSprites ; (ypos-scanline) < 4? lda SIndx1 sec sbc Scanline cmp #4 bcs .NoSprites ; (ypos-scanline) < 4? .DrawSprites NEWLINE ; Calculate # of lines to draw for each sprite ; Sprite Y - current scanline + sprite height lda SIndx0 beq .Empty0 ; sprite 0 is inactive? sec sbc Scanline clc adc SSize0 sta SIndx0 ; SIndx0 += SSize0 - Scanline .Empty0 lda SIndx1 beq .Empty1 ; sprite 1 is inactive? sec sbc Scanline clc adc SSize1 sta SIndx1 ; SIndx1 += SSize1 - Scanline .Empty1 ; Find out the maximum # of lines to draw ; by taking the maximum of the two sprite heights cmp SIndx0 bpl .Cmp1 ; sindx0 < sindx1? lda SIndx0 .Cmp1 tax ; X = # of lines left to draw ; Add total draw height to scanline count ; Saves time rather than 'inc scanline' each line clc adc Scanline sta Scanline .DrawNextScanline ; Make sure player 0 index is within bounds ldy SIndx0 cpy SSize0 bcs .Inactive0 ; index >= size? (or index < 0) ; Lookup pixels for player 0 lda (PData0),y ; Do WSYNC and then quickly store pixels for player 0 sta WSYNC sta GRP0 ; Lookup/store colors for player 0 lda (PColr0),y sta COLUP0 .DrawSprite1 ; Make sure player 1 index is within bounds ldy SIndx1 cpy SSize1 bcs .Inactive1 ; index >= size? (or index < 0) ; Lookup/store pixels and colors for player 1 ; Note that we are already 30-40 pixels into the scanline ; by this point... lda (PData1),y sta GRP1 lda (PColr1),y sta COLUP1 .Inactive1 ; Decrement the two sprite indices dey sty SIndx1 dec SIndx0 ; Repeat until we've drawn all the scanlines for this job dex bne .DrawNextScanline ; Free up both player objects by zeroing them out stx SIndx0 stx SIndx1 stx SSize0 stx SSize1 ; WSYNC and then clear sprite data NEWLINE ldx #0 stx GRP0 stx GRP1 rts ; No sprites were drawn; just exit .NoSprites NEWLINE rts .Inactive0 ; Alternate player 0 path when it is inactive sta WSYNC lda #0 sta GRP0 sta COLUP0 beq .DrawSprite1 ; always taken due to lda #0 ; Perform one sort iteration ; X register contains sort index (0 to NSprites-1) SwapSprites subroutine ; First compare Priority[i] and Priority[i+1] lda Priority0,x cmp Priority0+1,x bcs .CompareYPos ; If Priority[i] < Priority[i+1], do the swap ; anyway after resetting priorities lda #0 sta Priority0,x sta Priority0+1,x ; reset ldy Sorted0+1,x bcc .DoSwap ; swap due to priority .CompareYPos ; Compare Y[i] and Y[i+1] ldy Sorted0,x lda YPos0,y ldy Sorted0+1,x cmp YPos0,y bcc .NoSwap ; Y[i] < Y[i+1]? don't swap .DoSwap ; Swap Sorted[i] and Sorted[i+1] lda Sorted0,x ; A <- Sorted[i] sty Sorted0,x ; Y -> Sorted[i] sta Sorted0+1,x ; A -> Sorted[i+1] .NoSwap rts ; Read joystick movement and apply to object 0 MoveJoystick subroutine ; Move vertically ldx YPos0 lda #%00010000 ;Up? bit SWCHA bne .SkipMoveUp cpx #8 bcc .SkipMoveUp dex .SkipMoveUp lda #%00100000 ;Down? bit SWCHA bne .SkipMoveDown cpx #170 bcs .SkipMoveDown inx .SkipMoveDown stx YPos0 ; Move horizontally ldx XPos0 lda #%01000000 ;Left? bit SWCHA bne .SkipMoveLeft cpx #5 bcc .SkipMoveLeft dex .SkipMoveLeft lda #%10000000 ;Right? bit SWCHA bne .SkipMoveRight cpx #140 bcs .SkipMoveRight inx .SkipMoveRight stx XPos0 rts ; SetHorizPos - 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 ; NOTE: This version of the routine does a NEWLINE after executing ; because when the beam is at the far right side of the screen ; there is little time to do so before wrapping to the next line. ; It does NOT do a HMOVE and HCLR. SetHorizPos subroutine ; NEWLINE sta WSYNC ; start a new line inc Scanline 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 NEWLINE ; another WSYNC rts ; return to caller ; 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 #17 ; height .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; ; Enemy cat-head graphics data EnemyFrame0 .byte #0 .byte #%00111100;$AE .byte #%01000010;$AE .byte #%11100111;$AE .byte #%11111111;$AC .byte #%10011001;$8E .byte #%01111110;$8E .byte #%11000011;$98 .byte #%10000001;$98 ; Enemy cat-head color data EnemyColorFrame0 .byte #9 ; height .byte #$AE; .byte #$AC; .byte #$A8; .byte #$AC; .byte #$8E; .byte #$8E; .byte #$98; .byte #$94; ; Mapping of sprite objects (0-7) to sprite data SpriteDataMap .word Frame0,ColorFrame0 .word EnemyFrame0,EnemyColorFrame0 .word EnemyFrame0,EnemyColorFrame0 .word EnemyFrame0,EnemyColorFrame0 .word EnemyFrame0,EnemyColorFrame0 .word EnemyFrame0,EnemyColorFrame0 .word EnemyFrame0,EnemyColorFrame0 .word Frame1,ColorFrame0 ; Multiplication by 4 table ; faster than tya/asl/asl/tay MultBy4 .byte #$00,#$04,#$08,#$0c .byte #$10,#$14,#$18,#$1c .byte #$20,#$24,#$28,#$2c .byte #$30,#$34,#$38,#$3c ; Epilogue org $fffc .word Start .word Start