8bitworkshop/presets/examples/multisprite3.a

581 lines
15 KiB
Plaintext

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.
seg.u Variables
org $80
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
Sorted0 ds NSprites ; sorted list of sprite indices
Priority0 ds NSprites ; sprite priority list, if missed
MinYDist equ 7 ; min. Y distance to consider sprite
; Fetchs the approximate scanline (could be off by +/- 1)
; into A. Takes 11 or 14 cycles.
MAC GET_APPROX_SCANLINE
ldy INTIM
lda Timer2Scanline,y
bne .Ok
lda Timer2Scanline-1,y
.Ok
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 #19
iny
iny
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 24
; 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 CurIndex
stx SIndx0
stx SIndx1
stx SSize0
stx SSize1
TIMER_WAIT
; end of VBLANK
; Scanline loop
TIMER_SETUP 216 ; timer <- #$ff
lda #$90
sta COLUBK
NextFindSprite
; Try to schedule sprites to both players
jsr FindAnotherSprite
jsr FindAnotherSprite
; Apply fine offsets
sta WSYNC ; start next scanline
sta HMOVE ; apply the previous fine position(s)
; See if time to draw
jsr DrawSprites
; Repeat until all scanlines drawn
sta HMCLR ; reset the old horizontal position(s)
lda INTIM
cmp #$14 ; scanline 198
bcs NextFindSprite
lda #201 ; + 9 lines, end exactly
jsr WaitForScanline
; end of Scanline loop
NoMoreScanlines
; Clear all colors to black before overscan
ldx #0
stx COLUBK
stx COLUP0
stx COLUP1
stx COLUPF
; 30-2 lines of overscan
TIMER_SETUP 28
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?
; 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
; 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.
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
; Get index into SpriteDataMap (index * 4)
ldx MultBy4,y
; 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
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
ldx MultBy4,y
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
.SetupDone
inc CurIndex ; go to next sprite in sort order
.NoNearSprite
rts
; Draw any scheduled sprites.
DrawSprites subroutine
; Wait for next precise scanline
lda #0 ; 0 = wait for next
jsr WaitForScanline
lda Timer2Scanline,y ; lookup scanline #
sta Scanline ; save it
; 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
.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
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
; 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
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
rts ; return to caller
; Pass: A = desired scanline
; Returns: Y = timer value - 1
align $10
WaitForScanline subroutine
ldy INTIM ; Fetch timer value
.Wait
cpy INTIM
beq .Wait ; Wait for it to change
sta WSYNC ; Sync with scan line
cmp Timer2Scanline,y ; lookup scanline
bcs WaitForScanline ; repeat until >=
rts
; 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
; Timer -> Scanline table
align $100
Timer2Scanline
.byte 215, 0,214,213,212,211,210, 0,209,208,207,206,205,204, 0,203
.byte 202,201,200,199, 0,198,197,196,195,194, 0,193,192,191,190,189
.byte 188, 0,187,186,185,184,183, 0,182,181,180,179,178, 0,177,176
.byte 175,174,173,172, 0,171,170,169,168,167, 0,166,165,164,163,162
.byte 0,161,160,159,158,157,156, 0,155,154,153,152,151, 0,150,149
.byte 148,147,146, 0,145,144,143,142,141,140, 0,139,138,137,136,135
.byte 0,134,133,132,131,130, 0,129,128,127,126,125,124, 0,123,122
.byte 121,120,119, 0,118,117,116,115,114, 0,113,112,111,110,109,108
.byte 0,107,106,105,104,103, 0,102,101,100, 99, 98, 0, 97, 96, 95
.byte 94, 93, 92, 0, 91, 90, 89, 88, 87, 0, 86, 85, 84, 83, 82, 0
.byte 81, 80, 79, 78, 77, 76, 0, 75, 74, 73, 72, 71, 0, 70, 69, 68
.byte 67, 66, 0, 65, 64, 63, 62, 61, 60, 0, 59, 58, 57, 56, 55, 0
.byte 54, 53, 52, 51, 50, 0, 49, 48, 47, 46, 45, 44, 0, 43, 42, 41
.byte 40, 39, 0, 38, 37, 36, 35, 34, 0, 33, 32, 31, 30, 29, 28, 0
.byte 27, 26, 25, 24, 23, 0, 22, 21, 20, 19, 18, 0, 17, 16, 15, 14
.byte 13, 12, 0, 11, 10, 9, 8, 7, 0, 6, 5, 4, 3, 2, 0, 1
; Epilogue
org $fffc
.word Start
.word Start