Episode 6: Moving On
This episode we're moving on in order to implement the basic dynamic design, as in motions and player controls. But things are not that straight forward: Last episode, we thought we had positioning figured out, but the famous Battlezone repositioning routine proves to have some issues. Also, we discover some quirks in TIA timings, regarding when the various objects are actually displayed. However, we manage to figure it out, eventually, arriving at a decent half time result.
Since we finally have some interactive content, there's also an online demo showing the current state of affairs:
As we're making some progress on our project, we also progress beyond were it's easy to dwell on every assembler instruction in detail. So we'll pick up the interesting parts and show general concepts and discoveries, while leaving the details to the code listing at the end.
Horizontal Positioning Revisited
First, I implemented the Pong-style ball (or "bouncer", since it will collide with the other objects), simply progressing along 16-bit coordinates by vertical and horizontal deltas (the high-byte is used for screen coordinates, the low-byte allows us to move by fractions of pixels a frame). When colliding with the border, we bounce. (A Pong-like sound is yet to be imagined.)
On first glance, everything looked seemingly fine, but there were some irritations: Left of the 14th pixel (or so), there was some flicker. At closer inspection, the ball was jumping back and forth, while travelling through this left-most vertical segment. A sure sign of timing issues with our horizontal positioning routine. Moreover, the coordinates were not what we would have expected them to be. Odd! After all, this was the famous Battlezoney routine (or, rather, a table-based variant), referenced everywhere on the Web. Clearly, there were some issues, which hadn't shown up in our static alignment tests.
Hm.
So I put on the study cap for a closer audit of the code. It was not about page boundaries (which are required being crossed for the indexed lookup and are must not be crossed by the dvide-by-15 loop). Tested. If we move boundaries and memory alignments, things become even more odd. So, it must be about the timing, when there's no iteration of the devide-loop and we simply fall through, since our coordinates are of a low value. Having a closer look, the cycle counts commonly provided with the code were also off. The entire canonical interpretation of the code was not without issues. I tried to shift the timing und shuffle the code, and lo and behold, by inserting a NOP
(2 cycles) things began to look well.
Here's my revised version of the Battlezone repositioning routine:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Horizontal Positioning ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; org $F800 ;----------------------------- ; This table is on a page boundary to guarantee the processor ; will cross a page boundary and waste a cycle in order to be ; at the precise position ; (lookup index is negative underflow of 241...255, 0) fineAdjustBegin .byte %01110000 ; Left 7 .byte %01100000 ; Left 6 .byte %01010000 ; Left 5 .byte %01000000 ; Left 4 .byte %00110000 ; Left 3 .byte %00100000 ; Left 2 .byte %00010000 ; Left 1 .byte %00000000 ; No movement. .byte %11110000 ; Right 1 .byte %11100000 ; Right 2 .byte %11010000 ; Right 3 .byte %11000000 ; Right 4 .byte %10110000 ; Right 5 .byte %10100000 ; Right 6 .byte %10010000 ; Right 7 fineAdjustTable = fineAdjustBegin - %11110001 ; Note: %11110001 = -15 ; Battlezone style exact horizontal repositioning (modified) ; A = horizontal position in pixel ; X = object ; 0 = Player0 ; 1 = Player1 ; 2 = Missile0 ; 3 = Missile1 ; 4 = Ball bzoneRepos ; cycles sta WSYNC ; 3 wait for next scanline sec ; 2 start of scanline (0), set carry flag divideby15 sbc #15 ; 2 waste 5 cycles by dividing X-pos by 15 bcs divideby15 ; 2/3 now at 6/11/16/21/... tay ; 2 now at 8/13/18/23/... lda fineAdjustTable,Y ; 5 5 cycles, as we cross a page boundary nop ; 2 now at 15/20/25/30/... sta HMP0,X ; 4 store fine adjustment sta RESP0,X ; 4 (19/24/29/34/...) strobe position rts ; 6 ; Note: "bcs divideby15" must not cross a page boundary
However, new oddities emerged, namely regarding differences in positioning of the various sprites. Not all sprites are made equal by the TIA. They are implemented by polinomial counters of varying complexity, apparently resulting in varying timing. At least, when using the code above. This may be related to us now hitting the strobe for RESPx at cycle 19 for the earliest, while we're expected to to so at cycle 20. On the other hand, we're using an indexed store instruction, which probably accounts for an extra cycle before the data is actually written to the target address. The difference in offsets is probably due to the internal architecture of the TIA (there are several sources listing delays for strobes taking effect for various cobinations of registers and cycle counts). So we're going to make peace with the minor oddites in the coordinates and live with them. In respect to coordinates as we expect them, the ball and the two missiles are displayed a pixel (or TIA color cycle) early.
We get the following range of coordinates for the various sprites:
Player0, Player1 | 1...160 | |
Missile0, Missile1, Ball | 2...162 |
However.
At least, our ball is now moving smooth as butter and the ship coordinates are what they should be. We finally have arrived at a consistent model!
Reading Input
The next thing is to read some input to enter the terrific realms of interactivity. (The interesting part here is that every source will tell you how to read the button of the left joystick for Player0, but none will tell you about the button of the second one. Not even the Stella Programmer's Guide! It is only from the source code of the "vcs.h
" symbol definitions that we may learn how to read it. — By the way: Did you know that the VCS supports full keyboard scanning, including actively sending scan signals to an external keyboard controller?)
Here is how (from which registers) to read the joysticks, inputs are active LO and we have to configure the ports for input/read (as opposed to output).
Joysticks, register SWCHA (port A, set SWACNT to all zero for input):
Bit | Player0 | Player1 |
---|---|---|
D7 | right | |
D6 | left | |
D5 | down | |
D4 | up | |
D3 | right | |
D2 | left | |
D1 | down | |
D0 | up |
(Active LO: 1 = inactive, 0 = active.)
Joystick button, Player0, register INPT4 (bit D7, active LO):
INPT4 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
---|---|---|---|---|---|---|---|---|
Player0 | 1 | . | . | . | . | . | . | . |
Joystick button, Player1, register INPT5 (BIT D7, active LO):
INPT5 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
---|---|---|---|---|---|---|---|---|
Player1 | 1 | . | . | . | . | . | . | . |
Currently, we're reading only up, down, and fire, but we'll have to add left and right as soon as we implement the refraction effect. Pressing the button fires the missile, but by now it will not bounce. There's also a cooling effect for the missiles, which will block the button for a certain time. Pressing the button while the missile is still engaged, will reset the missile for a new shot. (In the final game, a player may either let a missile bounce or may abbort it and fire a new shot.)
Console Switches
Console switches are in SWCHB, bi-state latched inputs, active LO (port B, set SWBCNT to all zero for input)
Bit | Switch | Meaning |
---|---|---|
D7 | P1 difficulty | 0 = amateur (B), 1 = pro (A) |
D6 | P0 difficulty | 0 = amateur (B), 1 = pro (A) |
D5 | – not used – | |
D4 | – not used – | |
D3 | color - B/W | 0 = B/W, 1 = color |
D2 | – not used – | |
D1 | game select | 0 = switch pressed (active LO) |
D0 | game reset | 0 = switch pressed (active LO) |
Notably, we're all professionals by default and become amateurs by dementia, since difficulty A (up = 1) is “pro”. — The VCS was clearly designed with retro gaming in mind!
Currently, we've implemented the difficulty switches to select the shape of the ships individually per player and the reset switch.
Code
Reading inputs and setting up things accordingly is done in VBLANK, while moving existing objects, like missiles or the ball is done in overscan. The ships are moving vertically by one and a half pixel per frame. (We may want to implement a smooth acceleration and maybe a smooth fade-out of the moving action in one of the iterations to come.) Some further optimizations may be not completely out of discussion…
And here's the code, try it live here:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Program: Basic Screen + Motions ; System: Atari 2600 ; Source Format: DASM ; Author: N. Landsteiner, 2018 ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; processor 6502 include vcs.h include macro.h SEG.U config ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Constants ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; tv standard specifics ; uncomment for PAL ;PAL = 1 ifnconst PAL ;----------------------------- NTSC ; 262 lines: 3+37 VBlank, 192 kernel, 30 overscan ; timers (@ 64 cycles) ; VBlank 43 * 64 = 2752 cycles = 36.21 lines ; Overscan 35 * 64 = 2240 cycles = 29.47 lines ScanLines = 192 T64VBlank = 43 T64Overscan = 35 BorderHeight = 6 BorderClr = $64 ; purple ScoreClr = $EC ; yellow PlayerClr = $0C ; light grey PlayerClr2 = $9E ; light blue (unused) ;----------------------------- else ;----------------------------- PAL ; 312 lines: 3+45 VBlank, 228 kernel, 36 overscan ; timers (@ 64 cycles) ; VBlank 53 * 64 = 3392 cycles = 44.63 lines ; Overscan 42 * 64 = 2688 cycles = 35.36 lines ScanLines = 228 T64VBlank = 53 T64Overscan = 42 BorderHeight = 7 BorderClr = $C4 ScoreClr = $2C PlayerClr = $0C ; light grey PlayerClr2 = $BE ; light blue (unused) ;----------------------------- endif ; general definitions ScoresHeight = 10 PFHeight = ScanLines - ScoresHeight - 2 * BorderHeight shipVelocity = $0180 ballVelocityX = $0100 ballVelocityY = $0080 mslVelocity = $0200 mslCooling = $30 ; ship X coordinates (static) ship0X = 20 ship1X = 134 ; vars frCntr = $80 pfMask = $81 ; sprite coordinates (16-bit, HI-byte used for display) ; sprite specific horizontal offsets of TIA coordinates vs logical X: ; players: X+1 (1...160) ; missiles, ball: X+2 (2...161) ; (ball and missiles start 1 px left/early as compared to player sprites) ship0Y = $82 ; 2 bytes ship1Y = $84 ; 2 bytes ballX = $86 ; 2 bytes ballY = $88 ; 2 btyes ballDX = $8A ; 2 bytes ballDY = $8C ; 2 bytes msl0X = $8E ; 2 bytes msl0Y = $90 ; 2 bytes msl1X = $92 ; 2 bytes msl1Y = $94 ; 2 bytes msl0DX = $96 ; 2 bytes msl0DY = $98 ; 2 bytes msl1DX = $9A ; 2 bytes msl1DY = $9C ; 2 bytes msl0Cooling = $9E msl1Cooling = $9F ; addresses for relocated playfield scan line routine PFRoutine = $B0 ; where to place the scan line routine M1Ptr = PFRoutine + $03 S0Ptr = PFRoutine + $08 M0Ptr = PFRoutine + $0d S1Ptr = PFRoutine + $12 BlPtr = PFRoutine + $1f BrPtr = PFRoutine + $17 SEG cartridge ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Initialization ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; org $F000 Start sei ; disable interrupts cld ; clear BCD mode ldx #$FF txs ; reset stack pointer lda #$00 ldx #$28 ; clear TIA registers ($04-$2C) TIAClear sta $04,X dex bpl TIAClear ; loop exits with X=$FF ; ldx #$FF RAMClear sta $00,X ; clear RAM ($FF-$80) dex bmi RAMClear ; loop exits with X=$7F sta SWBCNT ; set console I/O to INPUT sta SWACNT ; set controller I/O to INPUT ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Game Init ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; lda #1 + 32 sta CTRLPF ; set up symmetric playfield, 2x ball width lda #0 sta pfMask sta frCntr lda #PlayerClr ; set player sprite colors sta COLUP0 sta COLUP1 lda #8 ; flip player 1 horizontally sta REFP1 jsr relocatePFRoutine lda #0 sta ship0Y sta ship1Y sta ballX sta ballY sta msl0Cooling sta msl1Cooling sta msl0X + 1 sta msl1X + 1 lda #PFHeight / 2 - 5 sta ship0Y + 1 sta ship1Y + 1 lda #81 ; 80 + 2 offset - 1 (size = 2) sta ballX + 1 lda #10 sta ballY + 1 lda #PFHeight sta msl0Y + 1 sta msl1Y + 1 lda #<ballVelocityX sta ballDX lda #>ballVelocityX sta ballDX + 1 lda #<ballVelocityY sta ballDY lda #>ballVelocityY sta ballDY + 1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Start a new Frame / VBLANK ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Frame lda #$02 sta WSYNC ; wait for horizontal sync sta VBLANK ; turn on VBLANK sta VSYNC ; turn on VSYNC sta WSYNC ; leave VSYNC on for 3 lines sta WSYNC sta WSYNC lda #$00 sta VSYNC ; turn VSYNC off lda #T64VBlank ; set timer for VBlank sta TIM64T ReadInput lda SWCHB and #1 ; D0: reset bne ShipSelect jmp Start ShipSelect ; set up ship base addresses by difficulty switches lda SWCHB and #$40 ; D6: difficulty player0 bne swd0 lda #<[Ship1 - PFHeight] sta S0Ptr lda #>[Ship1 - PFHeight] sta S0Ptr + 1 jmp swd1 swd0 lda #<[Ship2 - PFHeight] sta S0Ptr lda #>[Ship2 - PFHeight] sta S0Ptr + 1 swd1 lda SWCHB and #$80 ; D7: difficulty player1 bne swd2 lda #<[Ship1 - PFHeight] sta S1Ptr lda #>[Ship1 - PFHeight] sta S1Ptr + 1 jmp swd3 swd2 lda #<[Ship2 - PFHeight] sta S1Ptr lda #>[Ship2 - PFHeight] sta S1Ptr + 1 swd3 ReadJoysticks lda SWCHA ; joystick player1 and #%00010000 ; up? bne js0down ; active LO! sec lda ship0Y sbc #<shipVelocity sta ship0Y lda ship0Y + 1 sbc #>shipVelocity cmp #$F0 bcc js0st0 lda #0 js0st0 sta ship0Y + 1 js0down lda SWCHA and #%00100000 ; down? bne js0done clc lda ship0Y adc #<shipVelocity sta ship0Y lda ship0Y + 1 adc #>shipVelocity cmp #PFHeight-13 bcc js0st1 lda #PFHeight-12 js0st1 sta ship0Y + 1 js0done lda SWCHA ; joystick player1 and #%00000001 ; up? bne js1down sec lda ship1Y sbc #<shipVelocity sta ship1Y lda ship1Y + 1 sbc #>shipVelocity cmp #$F0 bcc js1st0 lda # js1st0 sta ship1Y + 1 js1down lda SWCHA and #%00000010 ; down? bne js1done clc lda ship1Y adc #<shipVelocity sta ship1Y lda ship1Y + 1 adc #>shipVelocity cmp #PFHeight-13 bcc js1st1 lda #PFHeight-12 js1st1 sta ship1Y + 1 js1done ReadButtons lda msl0Cooling ; missile 0 available? beq fire0 dec msl0Cooling jmp fire0done fire0 lda INPT4 ; check joystick 0 button bmi fire0done lda #mslCooling sta msl0Cooling lda ship0Y sta msl0Y lda ship0Y+1 clc adc #5 sta msl0Y+1 lda #ship0X+10 sta msl0X+1 lda #0 sta msl0X sta msl0DY sta msl0DY+1 lda #<mslVelocity sta msl0DX lda #>mslVelocity sta msl0DX+1 fire0done lda msl1Cooling ; missile 1 available? beq fire1 dec msl1Cooling jmp fire1done fire1 lda INPT5 ; check joystick 1 button bmi fire1done lda #mslCooling sta msl1Cooling lda ship1Y sta msl1Y lda ship1Y+1 clc adc #5 sta msl1Y+1 lda #ship1X-1 sta msl1X+1 lda #0 sta msl1X sta msl1DY sta msl1DY+1 sec sbc #<mslVelocity sta msl1DX lda #0 sbc #>mslVelocity sta msl1DX+1 fire1done VPositioning ; vertical sprite positions (off: y = PFHeight) lda S0Ptr clc adc ship0Y + 1 sta S0Ptr bcc s0Done inc S0Ptr + 1 s0Done lda S1Ptr clc adc ship1Y + 1 sta S1Ptr bcc s1Done inc S1Ptr + 1 s1Done lda #<[SpriteM - PFHeight] clc adc msl0Y + 1 sta M0Ptr lda #0 adc #>[SpriteM - PFHeight] sta M0Ptr + 1 lda #<[SpriteM - PFHeight] clc adc msl1Y + 1 sta M1Ptr lda #0 adc #>[SpriteM - PFHeight] sta M1Ptr + 1 lda #<[SpriteBL - PFHeight] clc adc ballY + 1 sta BlPtr lda #0 adc #>[SpriteBL - PFHeight] sta BlPtr + 1 HPositioning ; horizontal sprite positioning sta WSYNC lda #ship0X ; player0 ldx #0 jsr bzoneRepos lda #ship1X ; player1 ldx #1 jsr bzoneRepos lda msl0X + 1 ; missile0 ldx #2 jsr bzoneRepos lda msl1X + 1 ; missile1 ldx #3 jsr bzoneRepos lda ballX + 1 ; ball ldx #4 jsr bzoneRepos sta WSYNC VBlankWait lda INTIM bne VBlankWait ; wait for timer sta WSYNC ; finish current line sta HMOVE ; put movement registers into effect sta VBLANK ; turn off VBLANK ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Visible Kernel ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Scores ; just a dummy, render alternating lines ldy #ScoresHeight-1 ldx #0 stx COLUBK ScoresLoop sta WSYNC tya and #1 beq s1 lda #ScoreClr s1 sta COLUBK dey bpl ScoresLoop TopBorder sta WSYNC lda #BorderClr sta COLUBK sta COLUPF ; playfield color lda #16 ; playfield border (will not show in front of bg) sta PF0 lda pfMask sta PF1 sta BrPtr ldy #PFHeight-1 lda (BlPtr),Y ; load ball in advance dec BlPtr ; compensate for loading before dey in the pf-routine ldx #BorderHeight-1 topLoop sta WSYNC dex bne topLoop ; last line of border sleep 68 stx COLUBK ; we're exactly at the right border ; next scan-line starts Playfield jmp PFRoutine ; we'll start 3 cycles into the scan line, ; same as branch after WSYNC BottomBorder lda #BorderClr sta COLUBK lda #0 sta ENABL ; all sprites off sta ENAM0 sta ENAM1 sta GRP0 sta GRP1 sta PF0 ; playfield off sta PF1 sta PF2 ldy #BorderHeight btmLoop sta WSYNC dey bne btmLoop ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Overscan ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; OverscanStart lda #$02 sta VBLANK sta WSYNC lda #T64Overscan ; set timer for overscan sta TIM64T inc frCntr ; increment frame counter lda frCntr and #3 bne moveBallX lda pfMask ; flip playfield mask eor #1 sta pfMask moveBallX clc lda ballX adc ballDX sta ballX lda ballX + 1 adc ballDX + 1 sta ballX + 1 cmp #6 bcs ballRight lda #6 sta ballX + 1 bounceX sec lda #0 sta ballX sbc ballDX sta ballDX lda #0 sbc ballDX + 1 sta ballDX + 1 jmp moveBallY ballRight cmp #156 bcc moveBallY lda #155 sta ballX + 1 jmp bounceX moveBallY clc lda ballY adc ballDY sta ballY lda ballY + 1 adc ballDY + 1 sta ballY + 1 cmp #$F0 bcc ballBottom lda #0 sta ballY + 1 bounceY sec lda #0 sta ballY sbc ballDY sta ballDY lda #0 sbc ballDY + 1 sta ballDY + 1 jmp moveBallYDone ballBottom cmp #PFHeight-7 bcc moveBallYDone lda #PFHeight-7 sta ballY + 1 jmp bounceY moveBallYDone moveMissile0 lda msl0X +1 beq mvMsl0Done clc lda msl0X adc msl0DX sta msl0X lda msl0X+1 adc msl0DX+1 sta msl0X+1 cmp #158 bcc mvMsl0Done lda #0 sta msl0X+1 sta msl0Cooling lda #PFHeight sta msl0Y+1 mvMsl0Done moveMissile1 lda msl1X +1 beq mvMsl1Done clc lda msl1X adc msl1DX sta msl1X lda msl1X+1 adc msl1DX+1 sta msl1X+1 cmp #6 bcS mvMsl1Done lda #0 sta msl1X+1 sta msl1Cooling lda #PFHeight sta msl1Y+1 mvMsl1Done OverscanWait lda INTIM bne OverscanWait ; wait for timer jmp Frame ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Playfield Scan Line Routine ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; kernel scan-line routine to be relocated to RAM (addr. PFRoutine) ; relocation via 'rorg ... rend' breaks DASM (why?), so let's do it the hard way PFStart ; * = $B0 ;pfLoop hex 85 1f ; sta ENABL ; draw ball hex b9 00 00 ; lda 00,Y ; draw missile 1 hex 85 1e ; sta ENAM1 hex b9 00 00 ; lda 00,Y ; draw player 0 hex 85 1b ; sta GRP0 hex b9 00 00 ; lda 00,Y ; draw missile 0 hex 85 1d ; sta ENAM0 hex b9 00 00 ; lda 00,Y ; draw player 1 hex 85 1c ; sta GRP1 ;barrier hex a9 00 ; lda #0 ; flickering barrier animation: hex 49 01 ; eor #1 ; the two barriers will be out of sync, because hex 85 0e ; sta PF1 ; at this point we already missed PF1 at the left. hex 85 c7 ; sta barrier+1 ; store pattern with D0 flipped (self-modifying) hex b9 00 00 ; lda 00,Y ; load ball for next line hex 88 ; dey hex 85 02 ; sta WSYNC hex d0 da ; bne pfLoop ; start over 3 cycles into the scan line hex 4c ; jmp PFEnd ; 38 + 2 bytes in total ; subroutine to move it to RAM relocatePFRoutine ldx #PFEnd-PFStart mvCode lda PFStart,X sta PFRoutine,X dex bpl mvCode PfReturn = PFEnd - PFStart + PFRoutine lda #<BottomBorder ; fix up return vector sta PfReturn lda #>BottomBorder sta PfReturn+1 ; uncomment, if PFRoutine != $B0 ; lda #PFRoutine + $17 ; sta PFRoutine + $1d ; fix up the self-modifying rewrite addr rts ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Horizontal Positioning ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; org $F800 ;----------------------------- ; This table is on a page boundary to guarantee the processor ; will cross a page boundary and waste a cycle in order to be ; at the precise position ; (lookup index is negative underflow of 241...255, 0) fineAdjustBegin .byte %01110000 ; Left 7 .byte %01100000 ; Left 6 .byte %01010000 ; Left 5 .byte %01000000 ; Left 4 .byte %00110000 ; Left 3 .byte %00100000 ; Left 2 .byte %00010000 ; Left 1 .byte %00000000 ; No movement. .byte %11110000 ; Right 1 .byte %11100000 ; Right 2 .byte %11010000 ; Right 3 .byte %11000000 ; Right 4 .byte %10110000 ; Right 5 .byte %10100000 ; Right 6 .byte %10010000 ; Right 7 fineAdjustTable = fineAdjustBegin - %11110001 ; Note: %11110001 = -15 ; Battlezone style exact horizontal repositioning (modified) ; ; X = object A = position in px ; -------------------------------------- ; 0 = Player0 offset 1, 1...160 ; 1 = Player1 offset 1, 1...160 ; 2 = Missile0 offset 2, 2...161 ; 3 = Missile1 offset 2, 2...161 ; 4 = Ball offset 2, 2...161 bzoneRepos ; cycles sta WSYNC ; 3 wait for next scanline sec ; 2 start of scanline (0), set carry flag divideby15 sbc #15 ; 2 waste 5 cycles by dividing X-pos by 15 bcs divideby15 ; 2/3 now at 6/11/16/21/... tay ; 2 now at 8/13/18/23/... lda fineAdjustTable,Y ; 5 5 cycles, as we cross a page boundary nop ; 2 now at 15/20/25/30/... sta HMP0,X ; 4 store fine adjustment sta RESP0,X ; 4 (19/24/29/34/...) strobe position rts ; 6 ; Note: "bcs divideby15" must not cross a page boundary ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Data ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Sprite0 repeat PFHeight .byte $00 repend .byte $10 ; | X | .byte $10 ; | X | .byte $58 ; | X XX | .byte $BE ; |X XXXXX | .byte $73 ; | XXX XX| .byte $6D ; | XX XX X| .byte $73 ; | XXX XX| .byte $BE ; |X XXXXX | .byte $58 ; | X XX | .byte $10 ; | X | .byte $10 ; | X | Ship1 repeat PFHeight .byte $00 repend .byte $70 ; | XXX | .byte $78 ; | XXXX | .byte $5C ; | X XXX | .byte $9E ; |X XXXX | .byte $C3 ; |XX XX| .byte $BC ; |X XXXX | .byte $C3 ; |XX XX| .byte $9E ; |X XXXX | .byte $5C ; | X XXX | .byte $78 ; | XXXX | .byte $70 ; | XXX | Ship2 repeat PFHeight .byte $00 repend .byte $02 ; missile SpriteM repeat PFHeight .byte $00 repend .byte $02 ; ball .byte $02 .byte $02 .byte $02 .byte $02 .byte $02 .byte $02 SpriteBL repeat PFHeight .byte $00 repend .byte $00 SpriteEnd ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Interrupt and reset vectors ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; org $FFFA .word Start ; NMI .word Start ; Reset .word Start ; IRQ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; end ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
▶ Next: Episode 7: Let's Save Some Bytes!
◀ Previous: Episode 5: Let's Waste Some Bytes!
▲ Back to the index.
April 2018, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2018/04. —