; GEO-REFERENCED SPRITES WITH LATERAL MOTIONS ON THE C64 ; A lightweight model for fast execution via interrupts ; Suitable for bi-directionally scrolling platformers and shoot-em-ups where geo-referenced sprites with additional lateral motion are required ; By KODIAK https://kodiak64.com ; Use and modify as you require ; ; In the example herein, sprite 07 is geo-referenced and allowed (if required) to move horizontally, relative to a fixed point on a horizontally scrolling landscape ; Zero Page variables, all prefixed with "ZP", help speed up execution inside IRST ; ZPSPR07MODE = $02 ; 00 = Sprite 07 no visible on-screen (default), 01 = sprite 07 is visible on-screen ZPSCROLLDIRECTION = $03 ; 00 = Landscape not scrolling (i.e. stationary), 01 = scrolling from right to left, 02 = scrolling from left to right ZPMAPLOCATION = $04 ; ID of current char block being fed onto far RHS of screen ZPXPOSBLOCKCHAR = $05 ; Char position with 10 char block presently being fed onto edge of screen... values = 0 - 9 ZPSPR7XPOS = $06 ; Holds sprite 07's x-position value, which is written to the relevant VIC regesiter during the IRST ZPSPR7YPOS = $07 ; Holds sprite 07's y-position value, which is written to the relevant VIC regesiter during the IRST ZPSPRPOINTER7 = $08 ; Holds sprite 07's pointer value, which is written to the sprite 07 pointer location during the IRST ZPSPREXPANDX = $09 ; Holds bit value for VIC sprite x-expand register, to which this value is written during the IRST ZPSPREXPANDY = $0A ; Holds bit value for VIC sprite y-expand register, to which this value is written during the IRST ZPSPRMCMODE = $0B ; Holds bit value for VIC sprite multicolour-hires register, to which this value is written during the IRST ZPSPRENABLE = $0C ; Holds bit value for VIC sprite enable register, to which this value is written during the IRST ZPSPR07DATUMCOND = $0D ; 00 = Datum is to the left of the x-position = 0 and datum's MSB = 0 - in other words, to the left of the far LHS of the screen ; 01 = Datum is to the right of the x-position = 0 and datum's MSB = 0 ; 02 = Datum is to the right of the x-position = 0 and datum's MSB = 1, but to the left of the x-position = 89 (the far RHS of the screen) ZPSPR07XCOUNT = $0E ; This ranges from 0-79 (i.e. 80 pixels' worth of counting, so that the lateral movement routine (SPR07ROAMING) always reads the correct x-position increment value from the sine table ; S P R 0 7 F E E D ; FUNCTION: Feed-in instances of sprite 07 when trigger point on scrolling landscape scrolls in from the edge of the screen ; CALLED FROM: IRST once per frame, typically in the lower border while preparing the sprites for plexing in the next frame ; SPR07MAPLOC BYTE 008,014,022,032,240,251 ; These are obviously just random examples of map block numbers in which an instance of sprite 07 is geo-fixed SPR07MAPCLR BYTE 006,011,004,011,006,002 ; Sprite 07 colours SPR07MAPVERT BYTE 000,001,000,000,001,001 ; 00 = Geo-stationary only, 01 = L + R motion relative to fixed point on scrolling map SPR07MAPYPOS BYTE 120,055,080,090,040,120 ; Sprite 07 y-positions in pixels / scanlines SPR07FEED LDA ZPSPR07MODE BNE SPR07Q ; Quit edge-of-screen feeder routine if sprite 07 is already activated and on display LDA ZPSCROLLDIRECTION ; << 00 = STATIONARY, 1 = SCROLLING LEFT, 2 = SCROLLING RIGHT BEQ SPR07Q ; Quit if player's sprite(s) not moving, as that will mean no scrolling and no scrolling means no new sprites can ; At this point, the landscape is not static, i.e., it is definitely scrolling... so let's test which direction it is scrolling in ; CMP #01 BNE SPR07ETLHS ; Branch if landscape is not scrolling left ; Scrolling LEFT, so set Sprite 07 on far RHS ; LDA #%10000000 ; Set MSB = 1 for Sprite 07 LDX #$09 ; FOR SELF-MODIFYING THE CODE: ORA in immediate mode STX IRSTSETMSB ; Self-modify the instruction to ORA immediate STA IRSTSETMSB+1 ; Set immediate value for the sprite's MSB LDA #02 ; 02 = datum to right of MSB line STA SPR07ETDATUMCOND+1 LDX ZPMAPLOCATION ; Store present scroll map location in x-reg INX ; Increase it by 1 as we need to test the map block / tile on the edge of the screen (but you can remove or amend this to suit your own map feed methodology) LDA #08 ; Char position offset for map feed (assumes the map is based on tiles 10 chars wide) LDY #89 ; Set datum x-position = 89 (always this value on far RHS) STY SPR07INITXPOS+1 ; Prevent "ghosting" effect from previous sprite 07 instance upon fresh activation of sprite 07 JMP SPR07ETMAP ; Jump ahead now to SPR07ETMAP ; Scrolling RIGHT, so set Sprite 07 on far LHS ; SPR07ETLHS LDA #%01111111 ; Set MSB = 0 for Sprite 07 LDX #$29 ; FOR SELF-MODIFYING THE CODE: AND immediate mode STX IRSTSETMSB ; Self-modify the instruction to AND immediate STA IRSTSETMSB+1 ; Set immediate value for the sprite's MSB LDA #00 ; 00 = datum to left of far LHS line STA SPR07ETDATUMCOND+1 STA SPR07INITXPOS+1 ; Prevent "ghosting" effect from previous sprite 07 instance upon fresh activation of sprite 07 SEC SBC #80 ; Set datum x-position = 256-(80 i.e. sprite 07's maximum range in pixel from datum) TAY ; Transfer the result to the y-reg for later use LDA ZPMAPLOCATION ; Store present scroll map location in accumulator SEC SBC #05 ; Subtract 5 as it's on the opposite side of the screen (4 x 10 = 40 chars = screen width, so we test the block before that which is 50 chars to left of ZPMAPLOCATION... assuming we're using blocks / tiles that are 10 chars wide... so adjust according to your block or tile feed schema) TAX ; Transfer the result to the x-reg for later use LDA #00 ; Char position offset for L4 map feed, also used for the subtraction that follows to calculate datum's leftmost boundary beyond LHS ; Set map scan values ; SPR07ETMAP STA SPR07CHARPOS+1 ; This takes 4 clock cycles... if we use a Zero Page variable, it would be only 3, but then reading it when the code is at SPR07CHARPOS would also take 3 cycles, whereas reading it the way it's done here takes 2 cycles, so no cycle advantage either way, but at least this way avoids the use of a precious Zero Page memory address STX SPR07MAP+1 ; As above! ; Scan map for sprite 07 geo-positions ; LDX #00 SPR07CAN LDA SPR07MAPLOC,X ; Test table for map block IDs in which an instance of sprite 07 is located SPR07MAP CMP #$FF ; <<< SELF-MODIFIED BEQ SPR07CHECKCHAR ; Sprite 07 datum detected on map, so branch to find which char position within relevant feeder block it is in INX CPX #06 ; For extra clock cycle saving, this loop could be unrolled... for now, with a mere 6 instances of sprite 07 geo-mapped, it's reasonable to keep the loop closed BNE SPR07CAN SPR07Q RTS ; Now that we have scrolled the landscape to a block in which a sprite 07 instance is located, let's check for the exact char position sprite 07's datum is anchored to ; SPR07CHECKCHAR LDA ZPXPOSBLOCKCHAR SPR07CHARPOS CMP #$FF ; <<< SELF-MODIFIED BNE SPR07Q ; Activate ZPSPR07MODE + set up sprite for displaying on screen ; SPR07INITXPOS LDA #$00 ; <<< SELF-MODIFIED STA ZPSPR7XPOS LDA #01 ; Set this flag = 1, meaning that sprite 07 is now active, i.e., on display on screen STA ZPSPR07MODE SPR07ETDATUMCOND LDA #$FF ; <<< SELF-MODIFIED STA ZPSPR07DATUMCOND LDA SPR07MAPYPOS,X ; Set sprite 07's y-position STA ZPSPR7YPOS LDA #187 ; Set sprite 07 pointer STA ZPSPRPOINTER7 LDA SPR07MAPCLR,X ; Set sprite 07 colour STA VICSPR7COLOR1 STY ZPSPR07DATUMXPOS ; Set datum's x-position STX SPR07ROAMING+1 ; Record map ID of active instance of sprite 07 - used in the lateral motion tests performed by the routine SPR07ROAMING ; Ensure sprite 07 is unexpanded in x-direction ; LDA ZPSPREXPANDX ; ZPSPREXPANDX is the Zero Page variable holding the sprite x-expansion bit value, which is written to the VIC register $D01D in the raster interrupt while in the lower border AND #%01111111 ; Turn it OFF for sprite 07 STA ZPSPREXPANDX ; Ensure sprite 07 is unexpanded in y-direction ; LDA ZPSPREXPANDY ; ZPSPREXPANDY is the Zero Page variable holding the sprite y-expansion bit value, which is written to the VIC register $D017 in the raster interrupt while in the lower border AND #%01111111 ; Turn it OFF for sprite 07 STA ZPSPREXPANDY ; Ensure sprite 07 is in multicolour mode ; LDA ZPSPRMCMODE ; ZPSPRMCMODE is the Zero Page variable holding the sprite multicolour/hires mode bit value, which is written to the VIC register $D01C in the raster interrupt while in the lower border ORA #%10000000 ; Turn it ON for sprite 07 STA ZPSPRMCMODE ; Enable sprite 07 ; LDA ZPSPRENABLE ; ZPSPRENABLE is the Zero Page variable holding the sprite enable bit value, which is written to the VIC register $D015 in the raster interrupt while in the lower border ORA #%10000000 ; Turn it ON for sprite 07 STA ZPSPRENABLE SPR07CHARPOSQ RTS ; //////////////////////////////////////// ; S P R 0 7 L E F T ; FUNCTION: Scroll any active instance of sprite 07's datum in this direction <-- (from R to L) ; CALLED FROM: IRST every time landscape is scrolled, so that the datum moves perfectly synchronised with the scenery ; SPR07LEFT LDA ZPSPR07MODE BEQ SPR07CHARPOSQ ; Branch to nearest RTS ; Prepare to subtract "SCROLL STEP" from datum's x-position ; LDA ZPSPR07DATUMXPOS SEC SPR07LEFTSTEP SBC #01 ; <<< Immediate value is SELF-MODIFIED by scroll routine to match landscape scroll speed so as to save raster time in execution @ higher scroll speeds TAX BCS SPR07SETX ; If datum doesn't cross x = 0 zero line in a <-- | <-- (i.e. R to L) fashion, just branch to set the x-position ; Datum is crossing x = 0 line... <-- | <-- ; DEC ZPSPR07DATUMCOND ; Decrease the datum condition by 1 BCC SPR07SETX ; Here BCC saves 1* clock time compared to JMP, which it replaces for efficiency ; //////////////////////// ; S P R 0 7 R I G H T ; FUNCTION: Scroll any active instance of sprite 07's datum in this direction --> (from L to R) ; CALLED FROM: IRST every time landscape is scrolled, so that the datum moves perfectly synchronised with the scenery ; SPR07RIGHT LDA ZPSPR07MODE BEQ SPR07CHARPOSQ ; Branch to nearest RTS ; Prepare to add "SCROLL STEP" to datum's x-position ; LDA ZPSPR07DATUMXPOS CLC SPR07RIGHTSTEP ADC #01 ; <<< Immediate value is SELF-MODIFIED by scroll routine to match landscape scroll speed so as to save raster time in execution @ higher scroll speeds TAX BCC SPR07SETX ; If datum doesn't cross x = 0 zero line in a --> | --> (i.e. L to R) fashion, just branch to set the x-position ; datum is crossing x = 0 line... --> | --> ; INC ZPSPR07DATUMCOND ; Increase the datum condition by 1 ; //////////////////////// ; S P R 0 7 S E T X ; FUNCTION: Add an arbitrary x-distance to the datum line and use the result as the literal on-screen x-position of sprite 07 ; CALLED FROM: IRST via SPR07RIGHT or SPR07LEFT, depending on which direction the game is scrolling ; SPR07SETX STX ZPSPR07DATUMXPOS ; Store result of ZPSPR07DATUMXPOS plus or minus the SCROLL STEP back into ZPSPR07DATUMXPOS, thereby synchronising it with the landscape scrolling ; Perform boundary condition tests to check if sprite 07 switch-off is required ; LDA ZPSPR07DATUMCOND CMP #02 BNE SPR07SETXA ; If ZPSPR07DATUMCOND = 2 and ZPSPR07DATUMXPOS >= 89, then set ZPSPR07MODE = 0 ; CPX #89 BCS SPR07OFF BCC SPR07SETXB SPR07SETXA CMP #01 BEQ SPR07SETXB ; If ZPSPR07DATUMCOND = 0 and ZPSPR07DATUMXPOS = outer limit, set ZPSPR07MODE = 0 ; LDA ZPSPR07DATUMXPOS CLC ADC #80 ; Again, the decimal value of 80 is the number of pixels that sprite 07 can move away from the datum line in the x-direction BCC SPR07OFF SPR07SETXB TXA CLC SPR07ETMARGIN ADC #00 ; <<< SELF-MODIFIED TAY BCC SPR07ETNOCROSS ; If carry flag remains clear (i.e. result doesn't cross an x = 0 line), branch to SPR07ETNOCROSS ; CARRY IS SET, i.e. sprite 07 sprite crosses an x = 0 line ; If ZPSPR07DATUMCOND = 1 and result does set carry flag, then set ZPSPR7XPOS = result and sprite 07 MSB = 1 ; If ZPSPR07DATUMCOND = 0 and result does set carry flag, then set ZPSPR7XPOS = result and sprite 07 MSB = 0 ; LDA ZPSPR07DATUMCOND BNE SPR07ETMSB1 ; If ZPSPR07DATUMCOND not = 0, branch to SPR07ETMSB1 SPR07ETMSB0 LDA #%01111111 ; Set sprite 07 MSB = 0 LDX #$29 ; SELF-MODIFY THE CODE: Set AND immediate mode SPR07ETMSBJ STA IRSTSETMSB+1 ; Self-modify the instruction and its immediate value where it writes to the VIC's sprite MSB register in the IRST, typically in the lower border zone b STX IRSTSETMSB STY ZPSPR7XPOS ; Set sprite 07's x-position to result of datum's x-position plus the lateral distance calculated in SPR07ROAMING RTS SPR07ETMSBQ STA ZPSPR7XPOS ; Set sprite 07's x-position to zero RTS ; CARRY IS CLEAR, i.e. sprite 07 sprite not crossing an x = 0 line ; SPR07ETNOCROSS LDA ZPSPR07DATUMCOND BEQ SPR07ETMSBQ ; If ZPSPR07DATUMCOND = 0 and result doesn't set carry flag, then don't update sprite x-pos... Instead, just set it = 0 CMP #01 BEQ SPR07ETMSB0 ; If ZPSPR07DATUMCOND = 1 and result doesn't set carry flag, then set ZPSPR7XPOS = result and sprite 07 MSB = 0 ; If ZPSPR07DATUMCOND = 2 and result doesn't set carry flag, then set ZPSPR7XPOS = result and sprite 07 MSB = 1 ; SPR07ETMSB1 LDA #%10000000 ; Set sprite 07 MSB = 1 LDX #$09 ; SELF-MODIFY THE CODE: Set ORA immediate mode JMP SPR07ETMSBJ ; Jump to SPR07ETMSBJ ; If ZPSPR07DATUMCOND = 2 and result doesn't set carry flag and ZPSPR07DATUMXPOS < 89, then set ZPSPR7XPOS = result and sprite 07 MSB = 1 ; SPR07OFF LDA #00 ; Flag the fact that sprite 07 is no longer on display (i.e. it has been scrolled off-screen) STA ZPSPR07MODE LDA ZPSPRENABLE ; Turn off sprite 07 AND #%01111111 STA ZPSPRENABLE SPR07QUITQ RTS ; //////////////////////// ; S P R 0 7 R O A M I N G ; FUNCTION: Make sprite 07 move horizontally relative to the datum from L to R to L ad nauseum (if flagged to do so), and in a sine wave pattern to provide a sense of inertia ; CALLED FROM: Main Loop of game... no need for this to be called from the IRST if you have your main loop timings working sensibly, although you can instead call it from the IRST if you like, in which case remove the SPR07COUNT stuff ; SPR07DIR BYTE 01 ; 00 = Laterally move sprite 07 relative to its datum in the left to right direction, 01 = ditto but in right to left direction SPR07COUNT BYTE 02 ; Counter to ensure this routine is not executed on every instance of the game's main loop... your mileage may vary if you test this out from a main loop, so you'll likely need to alter this number until you get the desired speed of lateral motion on-screen SPR07ROAMPOS BYTE 0,0,0,0,1,0,1,0,0,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2, 2,1,2,1,2,1,2,1,2,1,2,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,1,0,1,0,0,0,0 ; SINE WAVE INCREMENTAL GAPS (IN PIXELS) SPR07ROAMING LDX #00 ; <<< SELF-MODIFIED by SPR07FEED routine LDA SPR07MAPVERT,X ; Test to see which kind of sprite 07 motion is required - 00 = Geo-stationary, 01 = L + R motion BEQ SPR07QUITQ LDA SPR07COUNT BNE SPR07ROAMING0 DEC SPR07COUNT RTS SPR07ROAMING0 LDA #02 STA SPR07COUNT LDX ZPSPR07XCOUNT ; Feed x-register with counter holding current position of sprite 07 on the sine table SPR07ROAMPOS LDA SPR07DIR BEQ SPR07ROAMINGRTL ; Branch to the right to left motions if SPR07DIR = 0; otherwise, perform the left to right motions ; Adjust the lateral distance by which sprite 07 will move AWAY FROM its datum line when SPR07SETX is called from the IRST ; LDA SPR07ETMARGIN+1 CLC ADC SPR07ROAMPOS,X STA SPR07ETMARGIN+1 ; Increment and test the counter holding current position of sprite 07 on the sine table SPR07ROAMPOS to see if it has reached its maximum range and if so, toggle the direction of travel of sprite 07 relative to its datum ; LDA #78 SEC INS ZPSPR07XCOUNT ; Illegal opcode used here to save some clock cycles... not ultra necessary when executing from the main loop, but it's a good habit to use them where they provide performance gains BEQ SPR07TOGGLEDIR1 RTS ; Adjust the lateral distance by which sprite 07 will move TOWARDS its datum line when SPR07SETX is called from the IRST ; SPR07ROAMINGRTL LDA SPR07ETMARGIN+1 SEC SBC SPR07ROAMPOS,X STA SPR07ETMARGIN+1 ; Decrement and test the counter holding current position of sprite 07 on the sine table SPR07ROAMPOS to see if it has reached its minimum range (i..e zero, back at the datum) and if so, toggle the direction of travel of sprite 07 relative to its datum ; DEC ZPSPR07XCOUNT LDX ZPSPR07XCOUNT BMI SPR07TOGGLEDIR0 RTS SPR07TOGGLEDIR0 LDA #00 SPR07TOGGLEDIRJ STA ZPSPR07XCOUNT ; Reset ZPSPR07XCOUNT for moving towards RHS ; Toggle direction sprite 07 is moving in, relative to the datum (obviously) ; LDA SPR07DIR EOR #01 STA SPR07DIR RTS SPR07TOGGLEDIR1 LDA #77 ; Reset ZPSPR07XCOUNT for moving towards LHS JMP SPR07TOGGLEDIRJ