mirror of
https://github.com/lscharen/iigs-game-engine.git
synced 2024-06-13 19:29:30 +00:00
All of the sprite rendering has been deferred down to the level of the tile drawing. Sprites are no longer drawn/erased, but instead a sprite sheet is generated in AddSprite and referenced by the renderer. Because there is no longer a single off-screen buffer that holds a copy of all the rendered sprites, the TileStore size must be expanded to hold a reference to the sprite data address fo each tile. This increase in data structure size require the TileStore to be put into its own bank and appropriate code restructuring. The benefits to the rewrite are significant: 1. Sprites are never drawn/erased off-screen. They are only ever drawn directly to the screen or play field. 2. The concept of "damaged" sprites is gone. Every dirty tile automatically renders just to portion of a sprite that it intersects. These two properties result in a substantial increase in throughput.
841 lines
28 KiB
ArmAsm
841 lines
28 KiB
ArmAsm
; Functions for sprite 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.
|
|
;
|
|
; NOTE: It may be posible to remove the sprite plane banks in the future and render directly from
|
|
; some small per-sprite graphic buffers. This would eliminate the need to erase/draw in
|
|
; the sprite planes and all drawing would go directly to the backing tiles. Need to
|
|
; figure out an efficient way to fall back when sprites are overlapping, though.
|
|
;
|
|
; All of the erasing must happen in an initial phase, because erasing a sprite could cause
|
|
; other sprites to be marked as "DAMAGED" which means they need to be drawn (similar to NEW state)
|
|
|
|
; What really has to happen in the various cases:
|
|
;
|
|
; When a sprite is added, it needs to
|
|
; * draw into the sprite buffer
|
|
; * add itself to the TS_SPRITE_FLAG bitfield on the tiles it occupies
|
|
; * mark the tiles it occupies as dirty
|
|
;
|
|
; When a sprite is updated (Tile ID or H/V flip flags), it needs to
|
|
; * erase itself from the sprite buffer
|
|
; * draw into the sprite buffer
|
|
; * mark the tiles it occupies as dirty
|
|
; * mark other sprites it intersects as DAMAGED
|
|
;
|
|
; When a sprite is moved, it needs to
|
|
; * erase itself from the sprite buffer at the old locations
|
|
; * remove itself from the TS_SPRITE_FLAG bitfields on the tiles it occupied
|
|
; * mark sprites that intersect as DAMAGED
|
|
; * draw into the sprite buffer at the new location
|
|
; * add itself to the TS_SPRITE_FLAG bitfield on the tiles it now occupies
|
|
; * mark the tiles it occupied as dirty
|
|
; * mark other sprites it intersects as DAMAGED
|
|
;
|
|
; When a sprite is removed, it needs to
|
|
; * erase itself from the sprite buffer at the old locations
|
|
; * remove itself from the TS_SPRITE_FLAG bitfields on the tiles it occupied
|
|
; * mark other sprites it intersects as DAMAGED
|
|
;
|
|
; The reason that things are broken into phases is that we have to handle all of the erasing first,
|
|
; set dirty tiles, identify DAMAGED sprites, and THEN perform the drawing. It is not possible to
|
|
; just do each sprite one at a time.
|
|
;
|
|
; Initialize the sprite data and mask banks (all data = $0000, all masks = $FFFF)
|
|
InitSprites
|
|
ldx #$FFFE
|
|
lda #0
|
|
:loop1 stal spritedata,x
|
|
dex
|
|
dex
|
|
cpx #$FFFE
|
|
bne :loop1
|
|
|
|
ldx #$FFFE
|
|
lda #$FFFF
|
|
:loop2 stal spritemask,x
|
|
dex
|
|
dex
|
|
cpx #$FFFE
|
|
bne :loop2
|
|
|
|
; Clear values in the sprite array
|
|
|
|
ldx #{MAX_SPRITES-1}*2
|
|
:loop3 stz _Sprites+TILE_STORE_ADDR_1,x
|
|
dex
|
|
dex
|
|
bpl :loop3
|
|
|
|
; Initialize the VBUFF address offsets in the data and mask banks for each sprite
|
|
;
|
|
; The internal grid 13 tiles wide where each sprite has a 2x2 interior square with a
|
|
; tile-size buffer all around. We pre-render each sprite with all four vert/horz flips
|
|
VBUFF_STRIDE_BYTES equ 13*4
|
|
VBUFF_TILE_ROW_BYTES equ 8*VBUFF_STRIDE_BYTES
|
|
VBUFF_SPRITE_STEP equ VBUFF_TILE_ROW_BYTES*3
|
|
VBUFF_SPRITE_START equ {8*VBUFF_TILE_ROW_BYTES}+4
|
|
|
|
ldx #{MAX_SPRITES-1}*2
|
|
lda #VBUFF_SPRITE_START
|
|
clc
|
|
:loop4 sta _Sprites+VBUFF_ADDR,x
|
|
adc #VBUFF_SPRITE_STEP
|
|
dex
|
|
dex
|
|
bpl :loop4
|
|
|
|
; Precalculate some bank values
|
|
jsr _CacheSpriteBanks
|
|
rts
|
|
|
|
; Run through the list of tile store offsets that this sprite was last drawn into and mark
|
|
; those tiles as dirty. The largest number of tiles that a sprite could possibly cover is 20
|
|
; (an unaligned 4x3 sprite), covering a 5x4 area of play field tiles.
|
|
;
|
|
; Y register = sprite record index
|
|
_ClearSpriteFromTileStore
|
|
ldx _Sprites+TILE_STORE_ADDR_1,y
|
|
bne *+3
|
|
rts
|
|
ldal TileStore+TS_SPRITE_FLAG,x ; Clear the bit in the bit field. This seems wasteful, but
|
|
and _SpriteBitsNot,y ; there is no indexed form of TSB/TRB and caching the value in
|
|
stal TileStore+TS_SPRITE_FLAG,x ; a direct page location, only saves 1 or 2 cycles per and costs 10.
|
|
jsr _PushDirtyTileX
|
|
|
|
ldx _Sprites+TILE_STORE_ADDR_2,y
|
|
bne *+3
|
|
rts
|
|
ldal TileStore+TS_SPRITE_FLAG,x
|
|
and _SpriteBitsNot,y
|
|
stal TileStore+TS_SPRITE_FLAG,x
|
|
jsr _PushDirtyTileX
|
|
|
|
ldx _Sprites+TILE_STORE_ADDR_3,y
|
|
bne *+3
|
|
rts
|
|
ldal TileStore+TS_SPRITE_FLAG,x
|
|
and _SpriteBitsNot,y
|
|
stal TileStore+TS_SPRITE_FLAG,x
|
|
jsr _PushDirtyTileX
|
|
|
|
ldx _Sprites+TILE_STORE_ADDR_4,y
|
|
bne *+3
|
|
rts
|
|
ldal TileStore+TS_SPRITE_FLAG,x
|
|
and _SpriteBitsNot,y
|
|
stal TileStore+TS_SPRITE_FLAG,x
|
|
jsr _PushDirtyTileX
|
|
|
|
ldx _Sprites+TILE_STORE_ADDR_5,y
|
|
bne *+3
|
|
rts
|
|
ldal TileStore+TS_SPRITE_FLAG,x
|
|
and _SpriteBitsNot,y
|
|
stal TileStore+TS_SPRITE_FLAG,x
|
|
jsr _PushDirtyTileX
|
|
|
|
ldx _Sprites+TILE_STORE_ADDR_6,y
|
|
bne *+3
|
|
rts
|
|
ldal TileStore+TS_SPRITE_FLAG,x
|
|
and _SpriteBitsNot,y
|
|
stal TileStore+TS_SPRITE_FLAG,x
|
|
jsr _PushDirtyTileX
|
|
|
|
ldx _Sprites+TILE_STORE_ADDR_7,y
|
|
bne *+3
|
|
rts
|
|
ldal TileStore+TS_SPRITE_FLAG,x
|
|
and _SpriteBitsNot,y
|
|
stal TileStore+TS_SPRITE_FLAG,x
|
|
jsr _PushDirtyTileX
|
|
|
|
ldx _Sprites+TILE_STORE_ADDR_8,y
|
|
bne *+3
|
|
rts
|
|
ldal TileStore+TS_SPRITE_FLAG,x
|
|
and _SpriteBitsNot,y
|
|
stal TileStore+TS_SPRITE_FLAG,x
|
|
jsr _PushDirtyTileX
|
|
|
|
ldx _Sprites+TILE_STORE_ADDR_9,y
|
|
bne *+3
|
|
rts
|
|
ldal TileStore+TS_SPRITE_FLAG,x
|
|
and _SpriteBitsNot,y
|
|
stal TileStore+TS_SPRITE_FLAG,x
|
|
jmp _PushDirtyTileX
|
|
|
|
; This function looks at the sprite list and renders the sprite plane data into the appropriate
|
|
; tiles in the code field. There are a few phases to this routine. The assumption is that
|
|
; any sprite that needs to be re-drawn has been marked as DIRTY or DAMAGED.
|
|
;
|
|
; A DIRTY sprite is one that has moved, so it needs to be erased/redrawn in the sprite
|
|
; buffer AND the tiles it covers marked for refresh. A DAMAGED sprite shared one or more
|
|
; tiles with a DIRTY sprite, so it needs to be redraw in the sprite buffer (but not erased!)
|
|
; and its tile do NOT need to be marked for refresh.
|
|
;
|
|
; In the first phase, we run through the list of dirty sprites and erase them from their
|
|
; OLD_VBUFF_ADDR. This clears the sprite plane buffers. We also iterate through the
|
|
; TILE_STORE_ADDR_X array and mark all of the tile store location that this sprite had occupied
|
|
; as dirty, as well as removing this sprite from the TS_SPRITE_FLAG bitfield.
|
|
;
|
|
; A final aspect is that any of the sprites indicated in the TS_SPRITE_FLAG are marked to be
|
|
; drawn in the next phase (since a portion of their content may have been erased if they overlap)
|
|
;
|
|
; In the second phase, the sprite is re-drawn into the sprite plane buffers and the appropriate
|
|
; Tile Store locations are marked as dirty. It is important to recognize that the sprites themselves
|
|
; can be marked dirty, and the underlying tiles in the tile store are independently marked dirty.
|
|
|
|
activeSpriteList equ blttmp
|
|
|
|
phase1 dw :phase1_0
|
|
dw :phase1_1,:phase1_2,:phase1_3,:phase1_4
|
|
dw :phase1_5,:phase1_6,:phase1_7,:phase1_8
|
|
dw :phase1_9,:phase1_10,:phase1_11,:phase1_12
|
|
dw :phase1_13,:phase1_14,:phase1_15,:phase1_16
|
|
|
|
:phase1_16
|
|
ldy activeSpriteList+30
|
|
jsr _DoPhase1
|
|
:phase1_15
|
|
ldy activeSpriteList+28
|
|
jsr _DoPhase1
|
|
:phase1_14
|
|
ldy activeSpriteList+26
|
|
jsr _DoPhase1
|
|
:phase1_13
|
|
ldy activeSpriteList+24
|
|
jsr _DoPhase1
|
|
:phase1_12
|
|
ldy activeSpriteList+22
|
|
jsr _DoPhase1
|
|
:phase1_11
|
|
ldy activeSpriteList+20
|
|
jsr _DoPhase1
|
|
:phase1_10
|
|
ldy activeSpriteList+18
|
|
jsr _DoPhase1
|
|
:phase1_9
|
|
ldy activeSpriteList+16
|
|
jsr _DoPhase1
|
|
:phase1_8
|
|
ldy activeSpriteList+14
|
|
jsr _DoPhase1
|
|
:phase1_7
|
|
ldy activeSpriteList+12
|
|
jsr _DoPhase1
|
|
:phase1_6
|
|
ldy activeSpriteList+10
|
|
jsr _DoPhase1
|
|
:phase1_5
|
|
ldy activeSpriteList+8
|
|
jsr _DoPhase1
|
|
:phase1_4
|
|
ldy activeSpriteList+6
|
|
jsr _DoPhase1
|
|
:phase1_3
|
|
ldy activeSpriteList+4
|
|
jsr _DoPhase1
|
|
:phase1_2
|
|
ldy activeSpriteList+2
|
|
jsr _DoPhase1
|
|
:phase1_1
|
|
ldy activeSpriteList
|
|
jsr _DoPhase1
|
|
:phase1_0
|
|
jmp phase1_rtn
|
|
|
|
; If this sprite has been MOVED or REMOVED, then clear its bit from the TS_SPRITE_FLAG in
|
|
; all of the tile store locations that it occupied on the previous frame and add those
|
|
; tile store locations to the dirty tile list.
|
|
_DoPhase1
|
|
lda _Sprites+SPRITE_STATUS,y
|
|
ora forceSpriteFlag
|
|
bit #SPRITE_STATUS_MOVED+SPRITE_STATUS_REMOVED
|
|
beq :no_clear
|
|
jsr _ClearSpriteFromTileStore
|
|
:no_clear
|
|
|
|
; Check to see if sprite was REMOVED If so, then this is where we return its Sprite ID to the
|
|
; list of open slots
|
|
|
|
lda _Sprites+SPRITE_STATUS,y
|
|
bit #SPRITE_STATUS_REMOVED
|
|
beq :out
|
|
|
|
lda #SPRITE_STATUS_EMPTY ; Mark as empty
|
|
sta _Sprites+SPRITE_STATUS,y
|
|
|
|
lda _SpriteBits,y ; Clear from the sprite bitmap
|
|
trb SpriteMap
|
|
|
|
ldx _OpenListHead
|
|
dex
|
|
dex
|
|
stx _OpenListHead
|
|
tya
|
|
sta _OpenList,x
|
|
sty _NextOpenSlot
|
|
:out
|
|
rts
|
|
|
|
; Second phase takes care of drawing the sprites and marking the tiles that will need to be merged
|
|
; with pixel data from the sprite plane
|
|
phase2 dw :phase2_0
|
|
dw :phase2_1,:phase2_2,:phase2_3,:phase2_4
|
|
dw :phase2_5,:phase2_6,:phase2_7,:phase2_8
|
|
dw :phase2_9,:phase2_10,:phase2_11,:phase2_12
|
|
dw :phase2_13,:phase2_14,:phase2_15,:phase2_16
|
|
|
|
:phase2_16
|
|
ldy activeSpriteList+30
|
|
jsr _DoPhase2
|
|
:phase2_15
|
|
ldy activeSpriteList+28
|
|
jsr _DoPhase2
|
|
:phase2_14
|
|
ldy activeSpriteList+26
|
|
jsr _DoPhase2
|
|
:phase2_13
|
|
ldy activeSpriteList+24
|
|
jsr _DoPhase2
|
|
:phase2_12
|
|
ldy activeSpriteList+22
|
|
jsr _DoPhase2
|
|
:phase2_11
|
|
ldy activeSpriteList+20
|
|
jsr _DoPhase2
|
|
:phase2_10
|
|
ldy activeSpriteList+18
|
|
jsr _DoPhase2
|
|
:phase2_9
|
|
ldy activeSpriteList+16
|
|
jsr _DoPhase2
|
|
:phase2_8
|
|
ldy activeSpriteList+14
|
|
jsr _DoPhase2
|
|
:phase2_7
|
|
ldy activeSpriteList+12
|
|
jsr _DoPhase2
|
|
:phase2_6
|
|
ldy activeSpriteList+10
|
|
jsr _DoPhase2
|
|
:phase2_5
|
|
ldy activeSpriteList+8
|
|
jsr _DoPhase2
|
|
:phase2_4
|
|
ldy activeSpriteList+6
|
|
jsr _DoPhase2
|
|
:phase2_3
|
|
ldy activeSpriteList+4
|
|
jsr _DoPhase2
|
|
:phase2_2
|
|
ldy activeSpriteList+2
|
|
jsr _DoPhase2
|
|
:phase2_1
|
|
ldy activeSpriteList
|
|
jsr _DoPhase2
|
|
:phase2_0
|
|
jmp phase2_rtn
|
|
|
|
_DoPhase2
|
|
lda _Sprites+SPRITE_STATUS,y
|
|
ora forceSpriteFlag
|
|
and #SPRITE_STATUS_ADDED+SPRITE_STATUS_MOVED+SPRITE_STATUS_UPDATED
|
|
beq :out
|
|
|
|
; Mark the appropriate tiles as dirty and as occupied by a sprite so that the ApplyTiles
|
|
; subroutine will combine the sprite data with the tile data into the code field where it
|
|
; can be drawn to the screen. This routine is also responsible for setting the specific
|
|
; VBUFF address for each sprite's tile sheet position
|
|
|
|
jmp _MarkDirtySprite
|
|
:out
|
|
rts
|
|
|
|
; Use the blttmp space to build the active sprite list. Since the sprite tiles are not drawn until later,
|
|
; it's OK to use that scratch space here. And it's just the right size, 32 bytes
|
|
RebuildSpriteArray
|
|
lda SpriteMap ; Get the bit field
|
|
|
|
; Unrolled loop to get the sprite index values that coorespond to the set bit positions
|
|
|
|
pea $FFFF ; end-of-list marker
|
|
]step equ 0
|
|
lup 4
|
|
ror
|
|
bcc :skip_1
|
|
pea ]step
|
|
:skip_1 ror
|
|
bcc :skip_2
|
|
pea ]step+2
|
|
:skip_2 ror
|
|
bcc :skip_3
|
|
pea ]step+4
|
|
:skip_3 ror
|
|
bcc :skip_4
|
|
pea ]step+6
|
|
:skip_4 beq :end_1
|
|
]step equ ]step+8
|
|
--^
|
|
:end_1
|
|
|
|
; Now pop the values off of the stack until reaching the sentinel value. This could be unrolled, but
|
|
; it is only done once per frame.
|
|
|
|
ldx #0
|
|
:loop
|
|
pla
|
|
bmi :out
|
|
sta blttmp,x
|
|
inx
|
|
inx
|
|
bra :loop
|
|
:out
|
|
stx ActiveSpriteCount
|
|
rts
|
|
|
|
forceSpriteFlag ds 2
|
|
_RenderSprites
|
|
|
|
; Check to see if any sprites have been added or removed. If so, then we regenerate the active
|
|
; sprite list. Since adding and removing sprites is rare, this is a worthwhile tradeoff, because
|
|
; there are several places where we want to iterate over the all of the sprites, and having a list
|
|
; and not have to constantly load and test the SPRITE_STATUS just to skip unused slots can help
|
|
; streamline the code.
|
|
|
|
lda #DIRTY_BIT_SPRITE_ARRAY
|
|
trb DirtyBits ; clears the flag, if it was set
|
|
beq :no_rebuild
|
|
jsr RebuildSpriteArray
|
|
|
|
:no_rebuild
|
|
|
|
; First step is to look at the StartX and StartY values. If the screen has scrolled, then it has
|
|
; the same effect as moving all of the sprites.
|
|
;
|
|
; OPTIMIZATION NOTE: Should check that the sprite actually changes position. If the screen scrolls
|
|
; by +X, but the sprite moves by -X (so it's relative position is unchanged), then
|
|
; it does NOT need to be marked as dirty.
|
|
|
|
stz forceSpriteFlag
|
|
lda StartX
|
|
cmp OldStartX
|
|
bne :force_update
|
|
|
|
lda StartY
|
|
cmp OldStartY
|
|
beq :no_change
|
|
|
|
:force_update
|
|
lda #SPRITE_STATUS_MOVED
|
|
sta forceSpriteFlag
|
|
:no_change
|
|
|
|
; Dispatch to the first phase of rendering the sprites. By pre-building the list, we know exactly
|
|
; how many sprite to process and they are in a contiguous array. So we on't have to keep track
|
|
; of an iterating variable
|
|
|
|
ldx ActiveSpriteCount
|
|
jmp (phase1,x)
|
|
phase1_rtn
|
|
|
|
; Dispatch to the second phase of rendering the sprites.
|
|
ldx ActiveSpriteCount
|
|
jmp (phase2,x)
|
|
phase2_rtn
|
|
|
|
; Sprite rendering complete
|
|
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
|
|
|
|
; Small initialization routine to cache the banks for the sprite data and mask
|
|
_CacheSpriteBanks
|
|
lda #>spritemask
|
|
and #$FF00
|
|
ora #^spritedata
|
|
sta SpriteBanks
|
|
rts
|
|
|
|
; This is 13 blocks wide
|
|
SPRITE_PLANE_SPAN equ 52 ; 256
|
|
|
|
; A = x coordinate
|
|
; Y = y coordinate
|
|
;GetSpriteVBuffAddr ENT
|
|
; jsr _GetSpriteVBuffAddr
|
|
; rtl
|
|
|
|
; A = x coordinate
|
|
; Y = y coordinate
|
|
;_GetSpriteVBuffAddr
|
|
; pha
|
|
; 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
|
|
; sta 1,s
|
|
; pla
|
|
; rts
|
|
|
|
; Version that uses temporary space (tmp15)
|
|
;_GetSpriteVBuffAddrTmp
|
|
; sta tmp15
|
|
; 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 tmp15
|
|
; rts
|
|
|
|
; Add a new sprite to the rendering pipeline
|
|
;
|
|
; The tile id in the range 0 - 511. The top 7 bits are used as sprite control bits
|
|
;
|
|
; Bit 9 : Horizontal flip.
|
|
; Bit 10 : Vertical flip.
|
|
; Bits 11 - 12 : Sprite Size Selector
|
|
; 00 - 8x8 (1x1 tile)
|
|
; 01 - 8x16 (1x2 tiles)
|
|
; 10 - 16x8 (2x1 tiles)
|
|
; 11 - 16x16 (2x2 tiles)
|
|
; Bit 13 : Reserved. Must be zero.
|
|
; Bit 14 : Reserved. Must be zero.
|
|
; Bit 15 : Low Sprite priority. Draws behind high priority tiles.
|
|
;
|
|
; When a sprite has a size > 8x8, the horizontal tiles are taken from the next tile index and
|
|
; the vertical tiles are taken from tileId + 32. This is why tile sheets should be saved
|
|
; with a width of 256 pixels.
|
|
;
|
|
; Single sprite are limited to 24 lines high because there are 28 lines of padding above and below the
|
|
; sprite plane buffers, so a sprite that is 32 lines high could overflow the drawing area.
|
|
;
|
|
; A = tileId + flags
|
|
; X = x position
|
|
; Y = y position
|
|
AddSprite ENT
|
|
phb
|
|
phk
|
|
plb
|
|
jsr _AddSprite
|
|
plb
|
|
rtl
|
|
|
|
_AddSprite
|
|
phx ; Save the horizontal position
|
|
ldx _NextOpenSlot ; Get the next free sprite slot index
|
|
bpl :open ; A negative number means we are full
|
|
|
|
plx ; Early out
|
|
sec ; Signal that no sprite slot was available
|
|
rts
|
|
|
|
:open
|
|
sta _Sprites+SPRITE_ID,x ; Keep a copy of the full descriptor
|
|
jsr _GetTileAddr ; This applies the TILE_ID_MASK
|
|
sta _Sprites+TILE_DATA_OFFSET,x
|
|
|
|
lda #SPRITE_STATUS_OCCUPIED+SPRITE_STATUS_ADDED
|
|
sta _Sprites+SPRITE_STATUS,x
|
|
|
|
tya
|
|
sta _Sprites+SPRITE_Y,x ; Y coordinate
|
|
pla ; X coordinate
|
|
sta _Sprites+SPRITE_X,x
|
|
|
|
; jsr _GetSpriteVBuffAddrTmp
|
|
; sta _Sprites+VBUFF_ADDR,x ; This is now pre-calculated since each sprite slot gets a fixed location
|
|
|
|
jsr _PrecalcAllSpriteInfo ; Cache sprite property values (simple stuff)
|
|
jsr _DrawSpriteSheet ; Render the sprite into internal space
|
|
|
|
; Mark the dirty bit to indicate that the active sprite list needs to be rebuilt in the next
|
|
; render call
|
|
|
|
lda #DIRTY_BIT_SPRITE_ARRAY
|
|
tsb DirtyBits
|
|
|
|
lda _SpriteBits,x ; Get the bit flag for this sprite slot
|
|
tsb SpriteMap ; Mark it in the sprite map bit field
|
|
|
|
txa ; And return the sprite ID
|
|
clc ; Mark that the sprite was successfully added
|
|
|
|
; We can only get to this point if there was an open slot, so we know we're not at the
|
|
; end of the list yet.
|
|
|
|
ldx _OpenListHead
|
|
inx
|
|
inx
|
|
stx _OpenListHead
|
|
ldy _OpenList,x ; If this is the end, then the sentinel value will
|
|
sty _NextOpenSlot ; get stored into _NextOpenSlot
|
|
|
|
rts
|
|
|
|
; Precalculate some cached values for a sprite. These are *only* to make other part of code,
|
|
; specifically the draw/erase routines more efficient.
|
|
;
|
|
; There are variations of this routine based on whether we are adding a new sprite, updating
|
|
; it's tile information, or changing its position.
|
|
;
|
|
; X = sprite index
|
|
_PrecalcAllSpriteInfo
|
|
lda _Sprites+SPRITE_ID,x
|
|
and #$2E00
|
|
xba
|
|
sta _Sprites+SPRITE_DISP,x ; use bits 9 through 13 for full dispatch
|
|
|
|
; Clip the sprite's bounding box to the play field size and also set a flag if the sprite
|
|
; is fully offs-screen or not
|
|
tay ; use the index we just calculated
|
|
lda _Sprites+SPRITE_X,x
|
|
bpl :pos_x
|
|
lda #0
|
|
:pos_x cmp ScreenWidth
|
|
bcs :offscreen ; sprite is off-screen, exit early
|
|
sta _Sprites+SPRITE_CLIP_LEFT,x
|
|
|
|
lda _Sprites+SPRITE_Y,x
|
|
bpl :pos_y
|
|
lda #0
|
|
:pos_y cmp ScreenHeight
|
|
bcs :offscreen ; sprite is off-screen, exit early
|
|
sta _Sprites+SPRITE_CLIP_TOP,x
|
|
|
|
lda _Sprites+SPRITE_X,x
|
|
clc
|
|
adc _SpriteWidthMinus1,y
|
|
bmi :offscreen
|
|
cmp ScreenWidth
|
|
bcc :ok_x
|
|
lda ScreenWidth
|
|
dec
|
|
:ok_x sta _Sprites+SPRITE_CLIP_RIGHT,x
|
|
|
|
lda _Sprites+SPRITE_Y,x
|
|
clc
|
|
adc _SpriteHeightMinus1,y
|
|
bmi :offscreen
|
|
cmp ScreenHeight
|
|
bcc :ok_y
|
|
lda ScreenHeight
|
|
dec
|
|
:ok_y sta _Sprites+SPRITE_CLIP_BOTTOM,x
|
|
|
|
stz _Sprites+IS_OFF_SCREEN,x ; passed all of the off-screen test
|
|
rts
|
|
|
|
:offscreen
|
|
lda #1
|
|
sta _Sprites+IS_OFF_SCREEN,x
|
|
rts
|
|
|
|
; Remove a sprite from the list. Just mark its STATUS as FREE and it will be
|
|
; picked up in the next AddSprite.
|
|
;
|
|
; A = Sprite ID
|
|
RemoveSprite ENT
|
|
phb
|
|
phk
|
|
plb
|
|
jsr _RemoveSprite
|
|
plb
|
|
rtl
|
|
|
|
_RemoveSprite
|
|
tax
|
|
|
|
_RemoveSpriteX
|
|
lda _Sprites+SPRITE_STATUS,x
|
|
ora #SPRITE_STATUS_REMOVED
|
|
sta _Sprites+SPRITE_STATUS,x
|
|
rts
|
|
|
|
; Update the sprite's flags. We do not allow the size of a sprite to be changed. That requires
|
|
; the sprite to be removed and re-added.
|
|
;
|
|
; A = Sprite ID
|
|
; X = Sprite Tile ID and Flags
|
|
UpdateSprite ENT
|
|
phb
|
|
phk
|
|
plb
|
|
jsr _UpdateSprite
|
|
plb
|
|
rtl
|
|
|
|
_UpdateSprite
|
|
phx ; swap X/A to be more efficient
|
|
tax
|
|
pla
|
|
|
|
_UpdateSpriteX
|
|
cpx #MAX_SPRITES*2 ; Make sure we're in bounds
|
|
bcc :ok
|
|
rts
|
|
|
|
:ok
|
|
_UpdateSpriteXnc
|
|
sta _Sprites+SPRITE_ID,x ; Keep a copy of the full descriptor
|
|
jsr _GetTileAddr ; This applies the TILE_ID_MASK
|
|
sta _Sprites+TILE_DATA_OFFSET,x
|
|
|
|
jsr _PrecalcAllSpriteInfo ; Cache stuff
|
|
|
|
lda _Sprites+SPRITE_STATUS,x
|
|
ora #SPRITE_STATUS_UPDATED
|
|
sta _Sprites+SPRITE_STATUS,x
|
|
|
|
rts
|
|
|
|
; 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
|
|
MoveSprite ENT
|
|
phb
|
|
phk
|
|
plb
|
|
jsr _MoveSprite
|
|
plb
|
|
rtl
|
|
|
|
_MoveSprite
|
|
phx ; swap X/A to be more efficient
|
|
tax
|
|
pla
|
|
|
|
_MoveSpriteX
|
|
cpx #MAX_SPRITES*2 ; Make sure we're in bounds
|
|
bcc :ok
|
|
rts
|
|
|
|
:ok
|
|
_MoveSpriteXnc
|
|
sta _Sprites+SPRITE_X,x ; Update the X coordinate
|
|
pha
|
|
tya
|
|
sta _Sprites+SPRITE_Y,x ; Update the Y coordinate
|
|
|
|
; pla
|
|
; jsr _GetSpriteVBuffAddrTmp ; A = x-coord, Y = y-coord
|
|
; ldy _Sprites+VBUFF_ADDR,x ; Save the previous draw location for erasing
|
|
; sta _Sprites+VBUFF_ADDR,x ; Overwrite with the new location
|
|
; tya
|
|
; sta _Sprites+OLD_VBUFF_ADDR,x
|
|
|
|
jsr _PrecalcAllSpriteInfo ; Can be specialized to only update (x,y) values
|
|
|
|
lda _Sprites+SPRITE_STATUS,x
|
|
ora #SPRITE_STATUS_MOVED
|
|
sta _Sprites+SPRITE_STATUS,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.
|
|
;
|
|
;
|
|
; Number of "off-screen" lines above logical (0,0)
|
|
; NUM_BUFF_LINES equ 24
|
|
|
|
MAX_SPRITES equ 16
|
|
SPRITE_REC_SIZE equ 46
|
|
|
|
; Mark each sprite as ADDED, UPDATED, MOVED, REMOVED depending on the actions applied to it
|
|
; on this frame. Quick note, the same Sprite ID cannot be removed and added in the same frame.
|
|
; A REMOVED sprite if removed from the sprite list during the Render call, so it's ID is not
|
|
; available to the AddSprite function until the next frame.
|
|
|
|
SPRITE_STATUS_EMPTY equ $0000 ; If the status value is zero, this sprite slot is available
|
|
SPRITE_STATUS_OCCUPIED equ $8000 ; Set the MSB to flag it as occupied
|
|
SPRITE_STATUS_ADDED equ $0001 ; Sprite was just added (new sprite)
|
|
SPRITE_STATUS_MOVED equ $0002 ; Sprite's position was changed
|
|
SPRITE_STATUS_UPDATED equ $0004 ; Sprite's non-position attributes were changed
|
|
SPRITE_STATUS_REMOVED equ $0008 ; Sprite has been removed.
|
|
|
|
SPRITE_STATUS equ {MAX_SPRITES*0}
|
|
TILE_DATA_OFFSET equ {MAX_SPRITES*2}
|
|
VBUFF_ADDR equ {MAX_SPRITES*4} ; Fixed address in sprite/mask banks
|
|
SPRITE_ID equ {MAX_SPRITES*6}
|
|
SPRITE_X equ {MAX_SPRITES*8}
|
|
SPRITE_Y equ {MAX_SPRITES*10}
|
|
; OLD_VBUFF_ADDR equ {MAX_SPRITES*12}
|
|
TILE_STORE_ADDR_1 equ {MAX_SPRITES*14}
|
|
TILE_STORE_ADDR_2 equ {MAX_SPRITES*16}
|
|
TILE_STORE_ADDR_3 equ {MAX_SPRITES*18}
|
|
TILE_STORE_ADDR_4 equ {MAX_SPRITES*20}
|
|
TILE_STORE_ADDR_5 equ {MAX_SPRITES*22}
|
|
TILE_STORE_ADDR_6 equ {MAX_SPRITES*24}
|
|
TILE_STORE_ADDR_7 equ {MAX_SPRITES*26}
|
|
TILE_STORE_ADDR_8 equ {MAX_SPRITES*28}
|
|
TILE_STORE_ADDR_9 equ {MAX_SPRITES*30}
|
|
TILE_STORE_ADDR_10 equ {MAX_SPRITES*32}
|
|
SPRITE_DISP equ {MAX_SPRITES*34} ; pre-calculated index for jmp (abs,x) based on sprite size
|
|
SPRITE_CLIP_LEFT equ {MAX_SPRITES*36}
|
|
SPRITE_CLIP_RIGHT equ {MAX_SPRITES*38}
|
|
SPRITE_CLIP_TOP equ {MAX_SPRITES*40}
|
|
SPRITE_CLIP_BOTTOM equ {MAX_SPRITES*42}
|
|
IS_OFF_SCREEN equ {MAX_SPRITES*44}
|
|
|
|
; Maintain the index of the next open sprite slot. This allows us to have amortized
|
|
; constant sprite add performance. A negative value means no slots are available.
|
|
_NextOpenSlot dw 0
|
|
_OpenListHead dw 0
|
|
_OpenList dw 0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,$FFFF ; List with sentinel at the end
|
|
|
|
_Sprites ds SPRITE_REC_SIZE*MAX_SPRITES
|