; MULTISPRITE LIBRARY ; 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. ; There are two separate multisprite kernels: ; MSpriteDraw1 - Single-line sprites, no playfield. ; This kernel requires TIMER_TABLE_SETUP at the 0th ; scanline, as it uses the timer to figure out the ; current scanline. No vertical clipping is performed. ; In fact, if sprites go past the bottom, it will ; mess up the vertical sync. ; MSpriteDraw2 - Double-line sprites with playfield. ; The playfield is updated and the sprites are positioned ; in 8-scanline segments. ; This kernel uses the timer internally, but does not ; use the timer table or TIMER_TABLE_SETUP. ; Sprites are clipped on the bottom edge, but disappear ; off the top edge. ; Your program must call MSpriteInit when it starts, ; and MSpriteFrame between every frame. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; seg.u Variables Scanline byte ; current scanline CurIndex byte ; current sprite # to try to schedule PData0 word ; pointer (lo/hi) to player 0 bitmap data PData1 word ; pointer to player 1 bitmap data PColr0 word ; pointer to player 0 color 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 Shape0 ds NSprites ; shape index Flags0 ds NSprites ; NUSIZ and reflection flags Sorted0 ds NSprites ; sorted list of sprite indices Priority0 ds NSprites ; sprite priority list, if missed ;MinYDist equ 0 ; min. Y distance to consider sprite (not used) ;MinYPos equ 1 ; TODO??? MinYDist equ 6 ; min. Y distance to consider sprite MinYPos equ 2+MinYDist PF0Ptr word ; pointer to playfield data PF1Ptr word ; pointer to playfield data PF2Ptr word ; pointer to playfield data PFIndex byte ; offset into playfield array PFCount byte ; lines left in this playfield segment ; temporary values for kernel ; TODO: share with global temporaries Temp byte Colp0 byte Colp1 byte tmpPF0 byte tmpPF1 byte tmpPF2 byte ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; seg Code ; call at start of program MSpriteInit subroutine ; Initialize initial sort order ldx #0 .loop txa sta Sorted0,x inx cpx #NSprites bne .loop rts ; call between frames MSpriteFrame subroutine ; 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 jsr ResetCounters stx CurIndex rts ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; SINGLE-LINE KERNEL MSpriteDraw1 subroutine .NextFindSprite ; Try to schedule sprites to both players jsr FindAnotherSprite ; Apply fine offsets sta WSYNC ; start next scanline sta HMOVE ; apply the previous fine position(s) sta HMCLR ; clear motion registers ldx #0 stx VDELP0 ; set vertical delay off stx VDELP1 ; set vertical delay off ; See if time to draw jsr DrawSprites1 ; Repeat until all scanlines drawn lda INTIM cmp #$1f bcs .NextFindSprite lda #191 jmp WaitForScanline ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; DOUBLE-LINE KERNEL WITH BACKGROUND MSpritePrefill2 subroutine lda #0 sta Scanline jsr FindAnotherSprite2 jsr FindAnotherSprite2 sta WSYNC sta HMOVE ; apply the previous fine position(s) sta HMCLR ; clear motion registers rts MSpriteDraw2 subroutine sta WSYNC sta PFIndex ; 24 * 4 scanlines = 96 2xlines lda #0 sta PFCount sta VDELP1 lda #1 sta VDELP0 ; updates to GRP0 will be delayed jmp .NewSprites .Draw8Lines ; Phase 0: Fetch PF0 byte jsr DrawSprites2 ldy PFIndex lda (PF0Ptr),y ; load PF0 sta tmpPF0 ; Phase 1: Fetch PF1 byte jsr DrawSprites2 ldy PFIndex lda (PF1Ptr),y ; load PF1 sta tmpPF1 ; Phase 2: Fetch PF2 byte jsr DrawSprites2 ldy PFIndex lda (PF2Ptr),y ; load PF2 sty PFIndex sta tmpPF2 ; Phase 3: Write PF0/PF1/PF2 registers jsr DrawSprites2 lda tmpPF0 sta PF0 lda tmpPF1 sta PF1 lda tmpPF2 sta PF2 ; Go to next scanline, unless playfield is done ; or unless this segment is done dec PFIndex bmi .NoMoreLines ; playfield done dec PFCount bpl .Draw8Lines ; keep drawing ; done drawing, reset player counters jsr ResetCounters .NewSprites lda PFIndex asl asl eor #$7f sbc #34 sta Scanline ; Scanlines = 127 - PFIndex*4 ; Set up 0-2 player objects taking up to 8 scanlines TIMER_SETUP 7 jsr FindAnotherSprite2 jsr CalcSpriteEnd ; Update playfield ldy PFIndex lda (PF0Ptr),y ; load PF0 -> X tax lda (PF1Ptr),y ; load PF1 -> tmp sta tmpPF1 lda (PF2Ptr),y ; load PF2 -> Y tay ; Apply fine offsets TIMER_WAIT ; wait for 8th scanlines and WSYNC sta HMOVE ; apply the previous fine position(s) sta HMCLR ; clear motion registers ; Store playfield registers stx PF0 lda tmpPF1 sta PF1 sty PF2 dec PFIndex ; no more playfield? bmi .NoMoreLines lda PFCount bne .Draw8Lines sta WSYNC ; one more line beq .NewSprites .NoMoreLines rts ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; SPRITE DRAWING ROUTINES ; used by kernels ; need to set SIndx0/1, SSize0/1, PData0/1, PColr0/1 ; all players must already be setup ; called by single-line kernel DrawSprites1 subroutine ; Wait for next precise scanline lda #0 ; 0 = wait for next .AnotherScanline jsr WaitForScanline lda Timer2Scanline,y ; lookup scanline # beq .AnotherScanline ; not if zero! sta Scanline ; save it DrawSprites1a ; 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 beq .NoSprites ; X = 0? we're done sta WSYNC ; next scanline DrawSprites1b .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 jsr ResetCounters sta WSYNC stx GRP0 stx GRP1 ; No sprites were drawn; just exit .NoSprites 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 ; called by 2-line kernel DrawSprites2 subroutine ; Fetch sprite 0 values lda SSize0 ; height in 2xlines sec isb SIndx0 ; INC yp0, then SBC yp0 bcs DoDraw0 ; inside bounds? lda #0 ; no, load the padding offset (0) DoDraw0 tay ; -> Y lda (PColr0),y ; color for both lines sta Colp0 ; -> colp0 lda (PData0),y ; bitmap for first line sta GRP0 ; -> [GRP0] (delayed due to VDEL) ; Fetch sprite 1 values lda SSize1 ; height in 2xlines sec isb SIndx1 ; INC yp0, then SBC yp0 bcs DoDraw1 ; inside bounds? lda #0 ; no, load the padding offset (0) DoDraw1 tay ; -> Y lda (PColr1),y ; color for both lines tax lda (PData1),y ; bitmap for first line tay ; WSYNC and store sprite values lda Colp0 ; still have about 30 cycles left... sta WSYNC sty GRP1 ; GRP0 is also updated due to VDELP0 flag stx COLUP1 sta COLUP0 ; Return to caller rts CalcSpriteEnd subroutine ; Calculate # of lines to draw for each sprite ; SIndx = 255 - ypos + scanline lda SIndx0 beq .zero0 sec sbc Scanline eor #$ff sta SIndx0 sec sbc SSize0 .zero0 sta Temp lda SIndx1 beq .zero1 sec sbc Scanline eor #$ff sta SIndx1 sec sbc SSize1 cmp Temp bmi .cmp1 ; sindx0 < sindx1? .zero1 lda Temp ; load higher number .cmp1 ; Compute the number of 8x lines in this section eor #$ff clc adc #1 lsr lsr sta PFCount rts ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; COMMON ROUTINES ResetCounters subroutine ldx #0 stx SSize0 stx SSize1 stx SIndx0 stx SIndx1 rts ; 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? ; Increment priority for this sort entry inc Priority0,x ; Go to next sort index, until we get to the end inx stx CurIndex .OutOfSprites rts ; Try to assign the next sprite in the sort order into ; one of the two player slots. ; If sprite found, uses at least 3 scanlines for SetHorizPos. FindAnotherSprite ; subroutine entry point ; Get the approximate scanline GET_APPROX_SCANLINE clc adc #MinYDist sta Scanline FindAnotherSprite2 ; alternate entry point when scanline known ; Calculate the distance to next sprite ldx CurIndex cpx #NSprites bcs .OutOfSprites ldy Sorted0,x ; get sprite index # in Y-sorted order lda YPos0,y ; get Y position of sprite cmp 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) ; 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. ; TODO: disable for 2-line sprite kernel cmp #34 ; X < 34 bcc .Plyr1NotReady ; First let's set its horizontal offset ldx #1 jsr SetHorizPos ; set horizontal position (does WSYNC) ; Assign the sprite's Y position to player 1 lda YPos0,y sta SIndx1 ; Set player 1 reflection/number/size flags lda Flags0,y sta REFP1 ; reflection flag sta NUSIZ1 ; number-size ; Get index into SpriteDataMap (index * 4) lda Shape0,y asl asl tax ; Copy addresses of pixel/color maps to player 1 lda SpriteDataMap,x sta PData1 lda SpriteDataMap+1,x sta PData1+1 lda SpriteDataMap+2,x sta PColr1 lda SpriteDataMap+3,x sta PColr1+1 ; Get the sprite height as the first byte of the color map ldy #0 lda (PColr1),y sta SSize1 inc SSize1 ; +1 to height 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 Flags0,y sta REFP0 ; reflection flag sta NUSIZ0 ; number-size lda Shape0,y asl asl tax lda SpriteDataMap,x sta PData0 lda SpriteDataMap+1,x sta PData0+1 lda SpriteDataMap+2,x sta PColr0 lda SpriteDataMap+3,x sta PColr0+1 ldy #0 lda (PColr0),y sta SSize0 inc SSize0 ; +1 to height .SetupDone inc CurIndex ; go to next sprite in sort order .NoNearSprite rts ; 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