iigs-game-engine/src/Sprite.s

525 lines
15 KiB
ArmAsm
Raw Normal View History

; Functions for sprie handling. Mostly maintains the sprite list and provides
; utility functions to calculate sprite/tile intersections
;
; The sprite plane actually covers two banks so that more than 32K can be used as a virtual
; screen buffer. In order to be able to draw sprites offscreen, the virtual screen must be
; wider and taller than the physical graphics screen.
;
; Initialize the sprite plane data and mask banks (all data = $0000, all masks = $FFFF)
InitSprites
ldx #$FFFE
lda #0
:loop1 stal spritedata,x
dex
dex
bpl :loop1
ldx #$FFFE
lda #$FFFF
:loop2 stal spritemask,x
dex
dex
bpl :loop2
rts
; This function looks at the sprite list and renders the sprite plane data into the appropriate
; tiles in the code field
_RenderSprites
ldx #0
:loop lda _Sprites+SPRITE_STATUS,x
beq :out
; cmp #SPRITE_STATUS_DIRTY
; beq :render
bra :render
:next inx
inx
bra :loop
:out rts
; This is the complicated part; we need to draw the sprite into the sprite plane, but then
; calculate the code field tiles that this sprite potentially overlaps with and mark those
; tiles as dirty and store the appropriate sprite plane address that those tiles need to copy
; from.
:render
stx tmp0 ; stash the X register
; jsr _EraseSprite ; erase from the old position
ldx tmp0
jsr _DrawTileSprite ; draw the sprite into the sprite plane
ldy #0 ; flags to mark if the sprite is aligned to the code field grid or not
lda _Sprites+SPRITE_X,x ; Will need some special handling for X < 0
sta tmp3
clc
adc StartXMod164
bit #$0003 ; If the botton bit are zero, then we're aligned
beq :aligned_x
ldy #4
:aligned_x
cmp #164
bcc *+5
sbc #164
lsr
lsr
pha ; Save the tile column
lda _Sprites+SPRITE_Y,x
sta tmp2
clc
adc StartYMod208
bit #$0007
beq :aligned_y
iny
iny
:aligned_y
cmp #208
bcc *+5
sbc #208
lsr
lsr
lsr
tyx ; stash the alignment in the x register for dispatch
jmp (:mark_dirty,x)
; :mark_dirty dw :corner,:column,:row,:square
:mark_dirty dw :corner,:corner,:corner,:corner
; Just mark the square with the sprite as dirty
:corner tay
plx
jsr _MarkAsDirty
ldx tmp0
brl :next
; Mark the left column (x, y) and (x, y+1) as dirty
:column tay
plx
jsr _MarkAsDirty
iny
cpy #26
bcc *+5
ldy #0
lda tmp2
clc
adc #8
sta tmp2
jsr _MarkAsDirty
ldx tmp0
brl :next
; Mark the top row (x, y) and (x+1, y) as dirty
:row tay
plx
jsr _MarkAsDirty
inx
cpx #41
bcc *+5
ldx #0
lda tmp3
clc
adc #4
sta tmp3
jsr _MarkAsDirty
ldx tmp0
brl :next
; Mark all four squares as dirty
:square tay
lda 1,s
tax
jsr _MarkAsDirty
inx
cpx #41
bcc *+5
ldx #0
lda tmp3
clc
adc #4
sta tmp3
jsr _MarkAsDirty
iny
cpy #26
bcc *+5
ldy #0
lda tmp2
clc
adc #8
sta tmp2
jsr _MarkAsDirty
plx
lda tmp3
sec
sbc #4
sta tmp3
jsr _MarkAsDirty
ldx tmp0
brl :next
_MarkAsDirty
phx
phy
jsr _GetTileStoreOffset ; Get the tile store value
jsr _PushDirtyTile ; Enqueue for processing (Returns offset in Y-register)
lda TileStore+TS_SPRITE_FLAG,y ; If this tile has already been flagged on this frame, avoid recalculating the address
beq :early_out
lda #TILE_SPRITE_BIT ; Mark this tile as having a sprite, regardless of whether it was already enqueued
sta TileStore+TS_SPRITE_FLAG,y
jsr _SetSpriteAddr
:early_out
ply
plx
rts
; Set the TileStore+TS_SPRITE_ADDR for tile that a sprite is on.
;
; To calculate the sprite plane coordinate for this tile column. We really just have to compensate
; for the StartXMod164 mod 4 value, so the final value is (SPRITE_X + (StartXMod164 mod 4)) & 0xFFFC
; for the horizontal and (SPRITE_Y + (StartYMod208 mod 8)) & 0xFFF8
;
; The final address is (Y + NUM_BUFF_LINES) * 256 + X
;
; tmp2 = sprite Y coordinate
; tmp3 = sprite X coordinate
; Y = tile record index
_SetSpriteAddr
lda StartYMod208
and #$0007
clc
adc tmp2
and #$00F8
clc
adc #NUM_BUFF_LINES
xba
sta tmp4
lda StartXMod164
and #$0003
clc
adc tmp3
and #$00FC
clc
adc tmp4
sta TileStore+TS_SPRITE_ADDR,y
rts
; _GetTileAt
;
; Given a relative playfield coordinate [0, ScreenWidth), [0, ScreenHeight) return the
; X = horizontal point [0, ScreenTileWidth]
; Y = vertical point [0, ScreenTileHeight]
;
; Return
; C = 1, out of range
; C = 0, X = column, Y = row
_GetTileAt
cpx ScreenWidth
bcc *+3
rts
cpy ScreenHeight
bcc *+3
rts
tya ; carry is clear here
adc StartYMod208 ; This is the code field line that is at the top of the screen
cmp #208
bcc *+5
sbc #208
lsr
lsr
lsr
tay ; This is the code field row for this point
clc
txa
adc StartXMod164
cmp #164
bcc *+5
sbc #164
lsr
lsr
tax ; Could call _CopyBG0Tile with these arguments
clc
rts
; _DrawSprite
;
; Draw the sprites on the _Sprite list into the Sprite Plane data and mask buffers. This is using the
; tile data right now, but could be replaced with compiled sprite routines.
_DrawSprites
ldx #0
:loop lda _Sprites+SPRITE_STATUS,x
beq :out ; The first open slot is the end of the list
cmp #SPRITE_STATUS_DIRTY
bne :skip
phx
lda _Sprites+VBUFF_ADDR,x ; Load the address in the sprite plane
ldy _Sprites+TILE_DATA_OFFSET,x ; Load the address in the tile data bank
tax
jsr _DrawTileSprite
plx
:skip
inx
inx
bra :loop
:out rts
DrawTileSprite ENT
jsr _DrawTileSprite
rtl
_DrawTileSprite
phb
pea #^tiledata ; Set the bank to the tile data
plb
]line equ 0
lup 8
lda: tiledata+32+{]line*4},y
andl spritemask+{]line*256},x
stal spritemask+{]line*256},x
ldal spritedata+{]line*SPRITE_PLANE_SPAN},x
and: tiledata+32+{]line*4},y
ora: tiledata+{]line*4},y
stal spritedata+{]line*SPRITE_PLANE_SPAN},x
lda: tiledata+32+{]line*4}+2,y
andl spritemask+{]line*SPRITE_PLANE_SPAN}+2,x
stal spritemask+{]line*SPRITE_PLANE_SPAN}+2,x
ldal spritedata+{]line*SPRITE_PLANE_SPAN}+2,x
and: tiledata+32+{]line*4}+2,y
ora: tiledata+{]line*4}+2,y
stal spritedata+{]line*SPRITE_PLANE_SPAN}+2,x
]line equ ]line+1
--^
plb ; pop extra byte
plb
rts
; Erase is easy -- set an 8x8 area of the data region to all $0000 and the corresponding mask
; resgion to all $FFFF
;
; A = sprite ID
SPRITE_PLANE_SPAN equ 256
_EraseSprite
asl
tay
ldx _Sprites+OLD_VBUFF_ADDR,y
phb ; Save the bank to switch to the sprite plane
pea #^spritedata
plb
lda #0
sta: {0*SPRITE_PLANE_SPAN}+0,x
sta: {0*SPRITE_PLANE_SPAN}+2,x
sta: {1*SPRITE_PLANE_SPAN}+0,x
sta: {1*SPRITE_PLANE_SPAN}+2,x
sta: {2*SPRITE_PLANE_SPAN}+0,x
sta: {2*SPRITE_PLANE_SPAN}+2,x
sta: {3*SPRITE_PLANE_SPAN}+0,x
sta: {3*SPRITE_PLANE_SPAN}+2,x
sta: {4*SPRITE_PLANE_SPAN}+0,x
sta: {4*SPRITE_PLANE_SPAN}+2,x
sta: {5*SPRITE_PLANE_SPAN}+0,x
sta: {5*SPRITE_PLANE_SPAN}+2,x
sta: {6*SPRITE_PLANE_SPAN}+0,x
sta: {6*SPRITE_PLANE_SPAN}+2,x
sta: {7*SPRITE_PLANE_SPAN}+0,x
sta: {7*SPRITE_PLANE_SPAN}+2,x
pea #^spritemask
plb
lda #$FFFF
sta: {0*SPRITE_PLANE_SPAN}+0,x
sta: {0*SPRITE_PLANE_SPAN}+2,x
sta: {1*SPRITE_PLANE_SPAN}+0,x
sta: {1*SPRITE_PLANE_SPAN}+2,x
sta: {2*SPRITE_PLANE_SPAN}+0,x
sta: {2*SPRITE_PLANE_SPAN}+2,x
sta: {3*SPRITE_PLANE_SPAN}+0,x
sta: {3*SPRITE_PLANE_SPAN}+2,x
sta: {4*SPRITE_PLANE_SPAN}+0,x
sta: {4*SPRITE_PLANE_SPAN}+2,x
sta: {5*SPRITE_PLANE_SPAN}+0,x
sta: {5*SPRITE_PLANE_SPAN}+2,x
sta: {6*SPRITE_PLANE_SPAN}+0,x
sta: {6*SPRITE_PLANE_SPAN}+2,x
sta: {7*SPRITE_PLANE_SPAN}+0,x
sta: {7*SPRITE_PLANE_SPAN}+2,x
pla
plb
rts
; Add a new sprite to the rendering pipeline
;
; A = tileId
; X = x position
; Y = y position
AddSprite ENT
phb
phk
plb
jsr _AddSprite
plb
rtl
_AddSprite
phx ; Save the horizontal position and tile ID
pha
ldx #0
:loop lda _Sprites+SPRITE_STATUS,x ; Look for an open slot
beq :open
inx
inx
cpx #MAX_SPRITES*2
bcc :loop
pla ; Early out
pla
sec ; Signal that no sprite slot was available
rts
:open lda #SPRITE_STATUS_DIRTY
sta _Sprites+SPRITE_STATUS,x ; Mark this sprite slot as occupied and that it needs to be drawn
pla
jsr _GetTileAddr
sta _Sprites+TILE_DATA_OFFSET,x
tya
clc
adc #NUM_BUFF_LINES ; The virtual buffer has 24 lines of off-screen space
xba ; Each virtual scan line is 256 bytes wide for overdraw space
clc
adc 1,s ; Add the horizontal position
sta _Sprites+VBUFF_ADDR,x
pla ; Pop off the saved value
clc ; Mark that the sprite was successfully added
txa ; And return the sprite ID
rts
; X = x coordinate
; Y = y coordinate
GetSpriteVBuffAddr ENT
tya
clc
adc #NUM_BUFF_LINES ; The virtual buffer has 24 lines of off-screen space
xba ; Each virtual scan line is 256 bytes wide for overdraw space
phx
clc
adc 1,s
plx
rtl
; Move a sprite to a new location. If the tile ID of the sprite needs to be changed, then
; a full remove/add cycle needs to happen
;
; A = sprite ID
; X = x position
; Y = y position
UpdateSprite ENT
phb
phk
plb
jsr _UpdateSprite
plb
rtl
_UpdateSprite
cmp #MAX_SPRITES*2 ; Make sure we're in bounds
bcc :ok
rts
:ok
stx tmp0 ; Save the horizontal position
and #$FFFE ; Defensive
tax ; Get the sprite index
lda #SPRITE_STATUS_DIRTY ; Position is changing, mark as dirty
sta _Sprites+SPRITE_STATUS,x ; Mark this sprite slot as occupied and that it needs to be drawn
lda _Sprites+VBUFF_ADDR,x ; Save the previous draw location for erasing
sta _Sprites+OLD_VBUFF_ADDR,x
lda tmp0 ; Update the X coordinate
sta _Sprites+SPRITE_X,x
tya ; Update the Y coordinate
sta _Sprites+SPRITE_Y,x
clc
adc #NUM_BUFF_LINES ; The virtual buffer has 24 lines of off-screen space
xba ; Each virtual scan line is 256 bytes wide for overdraw space
clc
adc tmp0 ; Add the horizontal position
sta _Sprites+VBUFF_ADDR,x
rts
; Sprite data structures. We cache quite a few pieces of information about the sprite
; to make calculations faster, so this is hidden from the caller.
;
; Each sprite record contains the following properties:
;
; +0: Sprite status word (0 = unoccupied)
; +2: Tile data address
; +4: Screen offset address (used for data and masks)
; Number of "off-screen" lines above logical (0,0)
NUM_BUFF_LINES equ 24
MAX_SPRITES equ 64
SPRITE_REC_SIZE equ 12
SPRITE_STATUS_EMPTY equ 0
SPRITE_STATUS_CLEAN equ 1
SPRITE_STATUS_DIRTY equ 2
SPRITE_STATUS equ 0
TILE_DATA_OFFSET equ {MAX_SPRITES*2}
VBUFF_ADDR equ {MAX_SPRITES*4}
SPRITE_X equ {MAX_SPRITES*6}
SPRITE_Y equ {MAX_SPRITES*8}
OLD_VBUFF_ADDR equ {MAX_SPRITES*10}
_Sprites ds SPRITE_REC_SIZE*MAX_SPRITES