Episode 8: Completing the Game Mechanics
We are finally seeing light at the end of what's not a tunnel: In this episode, we complete, if not finalize, the game mechanics. What's currently missing are the player controls for the eponymous refraction action and hit detection, including everything that comes with it, like object states, visual effects and score keeping.
Controlling Refractions
This is rather straight forward. Notwithstanding last episode's consideration, we go with the simple "asl ... rol
" scheme for multiplication. There's simply not much to gain otherwise. What we do add now to this, is a check of the joystick/controller states in register SWCHA (compare episode 6). The action, we're looking for first, is the player pulling the stick towards her side of the playfield. We could do this after we computed the distance of the missile from the vertical center and flip the results accordingly. However, this is a 16-bit value and it's probably less expensive to do the test in advance and to branch to a subtraction in the appropriate order of arguments, by this optaining the right sign without much overhead.
Next it's about augmenting the delta (or angle). If either of the two inputs for left or right are low (i.e. active), we add half of what we already determined in the beginning. Then, regardless of whether there had been any controller input at all, we proceed with the multiplication by 4, we discussed last time, and finally update the missile state to refect the fact that the missile has passed the barrier.
Hit Detection
Hit detection (or collisions) is implemented in the hardware of the TIA. If there are any objects overlapping in a scan line, bits in the internal collision registers are set accordingly. These are latches registers and we have to strobe register CXCLR to reset them, which we do just after the vertical sync signal (VSYNC) at the beginning of each frame. Therefor any collisions occuring in a frame, will be available in overscan, where we are going to check the registers.
Until now, we were mostly writing to the TIA (the controller ports and registers SWCHA, SWCHAB are part of the PIA/RIOT), But here, the TIA reveals a total different side, when we attempt to read from it. On read access, the TIA exhibits a set of collision registers and the input ports (we already know INPT4 and INPT5 for reading controller buttons) via a 6-bit address scheme:
TIA – Read Access
Register | Bits used (active HI) | Function | Semantics | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
6-bit Addr. | Name | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | D7 | D6 | |
0 | CXM0P | X | X | . | . | . | . | . | . | collision | M0–P1 | M0–P0 |
1 | CXM1P | X | X | . | . | . | . | . | . | collision | M1–P0 | M1–P1 |
2 | CXP0FB | X | X | . | . | . | . | . | . | collision | P0–PF | P0–BL |
3 | CXP1FB | X | X | . | . | . | . | . | . | collision | P1–PF | P1–BL |
4 | CXM0FB | X | X | . | . | . | . | . | . | collision | M0–PF | M0–BL |
5 | CXM1FB | X | X | . | . | . | . | . | . | collision | M1–PF | M1–BL |
6 | CXBLPF | X | . | . | . | . | . | . | . | collision | BL–PF | unused |
7 | CXPPMM | X | X | . | . | . | . | . | . | collision | P0–P1 | M0–M1 |
8 | INPT0 | X | . | . | . | . | . | . | . | pot port | ||
9 | INPT1 | X | . | . | . | . | . | . | . | pot port | ||
A | INPT2 | X | . | . | . | . | . | . | . | pot port | ||
B | INPT3 | X | . | . | . | . | . | . | . | pot port | ||
C | INPT4 | X | . | . | . | . | . | . | . | input (button) | ||
D | INPT5 | X | . | . | . | . | . | . | . | input (button) |
As may be observed, there's a bit to represent any of the collisions that may occur. For a ship, we're particularly interested in it colliding with the ball (CXM0FB, bit D6 for Player0) and with the opponent's missile (CXM1P, bit D7 for Player0). As the interesting bits are at the left or most significant end, we're going to check them using the sign bit (and a shift, when necessary):
CheckShipHit0 ; check collisions for ship0 lda ship0State bne shipHit0Done ; inactive lda CXM1P ; hit by missile1? bmi shipHit0 lda CXP0FB ; hit by ball? asl bpl shipHit0Done shipHit0 ldx #0 jsr ShipHit shipHit0Done
As we may see, there's a new subroutine for handling a hit and there's also a new variable to reflect the state of a ship. This is, because we're not simply resetting a ship, but we're going to insert an effect sequence to represent the game event. For this, we will also disable several objects, like missiles or the ball, while this sequence is in effect. Therefore, we add a state-variable for any of the objects and we put any code for handling any of those objects inside checks for the state. (As a side effect, we also rearrange some of the existing code, so that anything, which is related to a particular state may go inside a single check.)
Similarly, we arrange for the missiles, which will expire when hitting the ball. While we're at it, we add yet another 16-bit counter to countdown a missiles life until it finally expires, to prevent it from bouncing around for ever.
Simple Effects
We're using extensive amounts of memory for our approach to sprites, so we won't add another one (or two, or three) for an exploding ship. We're going for a flckering effect, which also matches the abstract style of our game. To keep things simple, we just switch the color of the player sprite. Towards the end, we reposition the ship at the vertical center and add a bit of a fade-in effect.
At this point, we reactivate all objects, we've previously disabled and also reset the ball.
Pitfall — But not by ActiVision
Repositioning the ball shouldn't be outrageously difficult. To provide a bit of variety, we will set up a random speed and direction (based on the frame counter) and also a random vertical start position (one out of eight). However, things aren't always that easy as expected. It's just a tiny bit of code, selecting one out of four possible velocities for any of the two axis, and we'll do so by simple table lookup. In the end, it looks like this:
ResetBall (...) ; get random bits and #3 ; reduce to 3 max asl ; now of 0, 2, 4, 6 tax lda ballVelocityX,X sta ballDX lda ballVelocityX + 1,X sta ballDX + 1 (...) ; get random bits and #3 asl tax lda ballVelocityY,X sta ballDY lda ballVelocityY + 1,X sta ballDY + 1 (...) ballVelocityX .word $0100 .word $0180 .word $FF00 .word $FE80 ballVelocityY .word $0080 .word $0100 .word $FE80 .word $FF00
The problem here, over which I spent considerable time, is the innocent assembler directive ".word
". It's the proper representation for 16-bit values. In my understanding it just represented two consecutive bytes (same as ".byte
" … ".byte
"). However, the DASM assembler isn't just bare-bones, it cares for the user and automatically converts a HI-byte/LO-byte notation into a LO-byte/HI-byte double in memory. For whatever reason, I didn't expect this and read the bytes in reverse order (in relation to the order in the source code), by this undoing what the assembler was doing for me already. Consequently, results were weird, to say the least. Since the HI-byte values exceeded the height of playfield, the ball not only moved erratically, but also showed up in multiple positions at once (also, in distorted shapes like bands). It was only after some considerable amount of head scratching that I discovered the source of the evil. — Another lesson learned.
However, in the end all was good. By this, we've implemented the basic game machanics in their entirety and there's also a basic, playable game. We may reconsider some of this after a bit of testing. E.g., we may want to move the barriers and the ship positions a bit towards the center, if we find the game too hard. We'll see.
By now, we're missing the score display (we're already mainting scores internally), a nice title screen, and, of course, sound.
- Try the live demo.
(BTW, I remapped the keys for the online emulation, as the old one proved to be rather tiresome.)
Code
And here's the code, so far:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Program: Refraction rev.0.5 ; Implements: Game Mechanics ; 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 ResetClr = $60 ; dark purple ;----------------------------- 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 ResetClr = $C0 ;----------------------------- endif ; general definitions ScoresHeight = 10 PFHeight = ScanLines - ScoresHeight - 2 * BorderHeight shipVelocity = $0180 mslVelocity = $0180 mslCooling = $30 ; frames MissileLife = $0140 ; frames ; ship X coordinates (static) ship0X = 20 ship1X = 134 ; vars frCntr = $80 toggle = $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 ; order and grouping is important for selecting objects by index ballX = $86 ; 2 bytes msl0X = $88 ; 2 bytes msl1X = $8A ; 2 bytes ballY = $8C ; 2 btyes msl0Y = $8E ; 2 bytes msl1Y = $90 ; 2 bytes ballDX = $92 ; 2 bytes msl0DX = $94 ; 2 bytes msl1DX = $96 ; 2 bytes ballDY = $98 ; 2 bytes msl0DY = $9A ; 2 bytes msl1DY = $9C ; 2 bytes msl0State = $9E msl0Cooling = $9F msl1State = $A0 msl1Cooling = $A1 msl0Life = $A2 msl1Life = $A4 ship0State = $A6 score1 = $A7 ship1State = $A8 score0 = $A9 ballState = $AA ; 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 toggle 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 sta ship0State sta ship1State sta score0 sta score1 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 jsr ResetBall ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; 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 sta CXCLR ; clear collision registers ReadInput lda SWCHB and #1 ; D0: reset bne ShipSelect jmp Start ShipSelect ; set up ship base addresses (select shape) lda SWCHB and #$8 ; D3: color/bw switch beq shipSelect2 shipSelect1 lda #<[Ship1 - PFHeight] sta S0Ptr lda #>[Ship1 - PFHeight] sta S0Ptr + 1 lda #<[Ship1 - PFHeight] sta S1Ptr lda #>[Ship1 - PFHeight] sta S1Ptr + 1 jmp shipSelectDone shipSelect2 lda #<[Ship2 - PFHeight] sta S0Ptr lda #>[Ship2 - PFHeight] sta S0Ptr + 1 lda #<[Ship2 - PFHeight] sta S1Ptr lda #>[Ship2 - PFHeight] sta S1Ptr + 1 shipSelectDone Ship0Handler ldx #0 ; payer0 lda ship0State beq ship0Active jsr ShipExplode jmp ship0Done ship0Active jsr SteerShip lda ship1State ; fire only, if opponent active bne ship0Done ldy INPT4 jsr FireMissile ship0Done Ship1Handler ldx #2 ; payer1 lda ship1State beq ship1Active jsr ShipExplode jmp ship1Done ship1Active jsr SteerShip lda ship0State ; fire only, if opponent active bne ship1Done ldy INPT5 jsr FireMissile ship1Done 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 toggle 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 CheckShipHit0 lda toggle ; flip toggle (barrier mask bit) eor #1 sta toggle ; collisions / hit detection CheckShipHit0 ; check collisions for ship0 lda ship0State bne shipHit0Done lda CXM1P ; hit by missile 1? bmi shipHit0 lda CXP0FB ; hit by ball? asl bpl shipHit0Done shipHit0 ldx #0 jsr ShipHit shipHit0Done CheckShipHit1 ; check collisions for ship1 lda ship1State bne shipHit1Done lda CXM0P ; hit by missile 0? bmi shipHit1 lda CXP1FB ; hit by ball? asl bpl shipHit1Done shipHit1 ldx #2 jsr ShipHit shipHit1Done CheckMslCollisions lda msl0State beq checkMsl1Bl lda msl1State beq checkMsl0Bl lda CXPPMM asl bpl checkMsl0Bl lda #0 jsr ResetMissile lda #2 jsr ResetMissile jmp checkMslDone checkMsl0Bl lda msl0State beq checkMsl1Bl lda CXM0FB asl bpl checkMsl1Bl lda #0 jsr ResetMissile checkMsl1Bl lda msl1State beq checkMslDone lda CXM1FB asl bpl checkMslDone lda #1 jsr ResetMissile checkMslDone ; motions MoveBall lda ballState beq moveBallDone ; inactive ldx #0 ; select ball X jsr MoveObject ldx #6 ; select ball Y jsr MoveObject moveBallDone MoveMissile0 ldx #0 lda msl0State beq moveMsl0Done ; inactive bpl moveMsl0Cnt ; already refracted lda msl0X + 1 cmp #116 ; crossed the barrier? bcc moveMsl0Cnt jsr Refract jmp moveMsl0 moveMsl0Cnt dec msl0Life ; count down (16-bit) towards expery bne moveMsl0 dec msl0Life + 1 bpl moveMsl0 jsr ResetMissile jmp moveMsl0Done moveMsl0 ldx #2 ; select missile0 Y jsr MoveObject ldx #8 ; select missile0 Y jsr MoveObject moveMsl0Done MoveMissile1 ldx #2 lda msl1State beq moveMsl1Done ; inactive bpl moveMsl1Cnt ; already refracted lda msl1X + 1 cmp #44 ; crossed the barrier? bcs moveMsl1Cnt jsr Refract jmp moveMsl1 moveMsl1Cnt dec msl1Life ; count down (16-bit) towards expery bne moveMsl1 dec msl1Life + 1 bpl moveMsl1 jsr ResetMissile jmp moveMsl1Done moveMsl1 ldx #4 ; select missile1 X jsr MoveObject ldx #10 ; select missile1 Y jsr MoveObject moveMsl1Done OverscanWait lda INTIM bne OverscanWait ; wait for timer jmp Frame ; some subroutines SteerShip ; X: ship (0, 2) lda ctrlYPlayer0,X and SWCHA ; joystick up? bne steerDown ; active LO! sec lda ship0Y,X sbc #<shipVelocity sta ship0Y,X lda ship0Y + 1,X sbc #>shipVelocity cmp #$F0 bcc steerSaveX lda #0 steerSaveX sta ship0Y + 1,X steerDown lda ctrlYPlayer0+1,X and SWCHA ; joystick down? bne steerDone clc lda ship0Y,X adc #<shipVelocity sta ship0Y,X lda ship0Y + 1,X adc #>shipVelocity cmp #PFHeight - 13 bcc steerSaveY lda #PFHeight - 12 steerSaveY sta ship0Y + 1,X steerDone rts MoveObject ; subroutine to move an object (x selects object and axis) clc ; DX: 0 ball, 2 missile0, 4 missile1 lda ballX,X ; DY: 6 ball, 8 missile0, 10 missile1 adc ballDX,X sta ballX,X lda ballX + 1,X adc ballDX + 1,X sta ballX + 1,X ldy ballDX + 1,X bpl moveInc ; branch on positive delta (incrementing) moveDec ldy minMaxBallX,X ; are we comparing to zero? beq moveCmp0 cmp minMaxBallX,X ; lower boundary from table bcs moveDone ; branch on greater or equal than boundary lda minMaxBallX,X ; new value = boundary jmp Bounce moveCmp0 cmp #$F0 ; deal with wrap around bcc moveDone ; branch on less than $F0 lda #0 jmp Bounce moveInc cmp minMaxBallX+1,X ; upper boundary from table bcc moveDone ; branch on less than boundary lda minMaxBallX+1,X sbc #1 ; new value = boundary - 1; carry already set jmp Bounce moveDone rts Bounce ; (sub)routine to invert an object's motion sta ballX + 1,X ; A: new pos HI-btye lda #0 ; X = DX: 0 ball, 2 missile0, 4 missile1 sta ballX,X ; DY: 6 ball, 8 missile0, 10 missile1 sec sbc ballDX,X sta ballDX,X lda #0 sbc ballDX + 1,X sta ballDX + 1,X rts FireMissile ; X = ship/player (0, 2), button input in Y lda msl0Cooling,X ; missile available? beq fire dec msl0Cooling,X rts fire tya bmi fireDone lda #mslCooling sta msl0Cooling,X lda ship0Y,X sta msl0Y,X lda ship0Y + 1,X clc adc #5 sta msl0Y + 1,X lda originMsl0,X sta msl0X + 1,X lda #0 sta msl0X,X sta msl0DY,X sta msl0DY + 1,X lda msl0Velocity,X sta msl0DX,X lda msl0Velocity + 1,X sta msl0DX + 1,X lda #$FF sta msl0State,X lda #<MissileLife sta msl0Life,X lda #>MissileLife sta msl0Life + 1,X fireDone rts Refract sec lda ctrlXPlayer0,X and SWCHA beq refractInv ; stick pulled lda #PFHeight/2 + 5 sbc msl0Y + 1,X jmp refractDif refractInv lda msl0Y + 1,X sbc #PFHeight/2 + 5 refractDif bcs refractAdd sec sbc #$20 sta msl0DY,X lda #$FF sta msl0DY + 1,X jmp refractCtrl refractAdd clc adc #20 sta msl0DY,X refractCtrl lda ctrlXPlayer0 + 1,X ; check, if stick pushed or pulled and SWCHA cmp ctrlXPlayer0 + 1,X beq refractMult asl msl0DY + 1,X ; DY x 1.5, get carry (DY+1 is either $FF or 0) lda msl0DY,X ror clc adc msl0DY,X sta msl0DY,X bcc refractMult inc msl0DY + 1,X refractMult ; multiply by 4 (2 16-bit shifts left) asl msl0DY,X rol msl0DY + 1,X asl msl0DY,X rol msl0DY + 1,X lda #1 ; update missile state sta msl0State,X refractEnd rts ShipHit ; ship in X (0, 2) lda #0 sta msl1State sta msl0State sta msl0Cooling sta msl1Cooling lda #PFHeight sta msl0Y + 1 sta msl1Y + 1 lda #$58 sta ship0State,X inc score1,X ; score0, score1 are stored in reverse order lda #0 ; ball off sta ballState lda #PFHeight sta ballY + 1 rts ResetShip ; ship in X (0, 2) lda #16 sta msl0Cooling sta msl1Cooling lda #PlayerClr cpx #0 bne resetShip1 sta COLUP0 jmp resetShipDone resetShip1 sta COLUP1 resetShipDone jmp ResetBall ShipExplode ; ship in X (0, 2) dec ship0State,X ; decrement counter beq ResetShip ; done with countdown, jump to reset cmp #34 ; equal 34? beq shipExRst ; yes, reset ship to center bcs shipExFlicker ; greater than 24, set blinking color cmp #8 ; is it the 8th-last frame? bne shipExDone ; no, return lda #BorderClr ; set ship to border color jmp shipExSetClr shipExFlicker lda frCntr ; select either PlayerClr (light) or ResetClr (dark) and #4 ; change every 4th frame beq shipExDark lda #PlayerClr jmp shipExSetClr shipExRst lda #0 sta ship0Y,X lda #PFHeight / 2 - 5 sta ship0Y + 1,X shipExDark lda #ResetClr shipExSetClr ; set the ship color cpx #0 bne shipEx1 sta COLUP0 jmp shipExDone shipEx1 sta COLUP1 shipExDone rts ResetMissile ; missile in X (0, 2) lda #0 sta msl0State,X sta msl0Cooling,X sta msl0Y,X lda #PFHeight sta msl0Y + 1,X rts ResetBall lda frCntr tay and #3 asl tax lda ballVelocityX,X sta ballDX lda ballVelocityX + 1,X sta ballDX + 1 tya ror ror tay and #3 asl tax lda ballVelocityY,X sta ballDY lda ballVelocityY + 1,X sta ballDY + 1 lda #0 sta ballY sta ballX sta ballY + 1 tya ror ror eor frCntr and #7 tax clc rstBallLoop adc #PFHeight/9 dex bpl rstBallLoop sta ballY + 1 lda #81 sta ballX + 1 lda #1 sta ballState rts ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Tables for subroutines / object selection ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; table joystick test patterns ctrlYPlayer0 .byte %00010000 ; up .byte %00100000 ; down ctrlYPlayer1 .byte %00000001 ; up .byte %00000010 ; down ctrlXPlayer0 .byte %01000000 ; left .byte %11000000 ; left or right ctrlXPlayer1 .byte %00001000 ; right .byte %00001100 ; left or right ; table of boundaries for various motions minMaxBallX .byte 6 .byte 156 minMaxMsl0X .byte 162-40-4 .byte 158 minMaxMsl1X .byte 6 .byte 40+4+2 minMaxBallY .byte 0 .byte PFHeight - 7 minMaxMsl0Y .byte 4 .byte PFHeight - 4 minMaxMsl1Y .byte 4 .byte PFHeight - 4 originMsl0 .byte ship0X + 10 .byte 0 originMsl1 .byte ship1X - 1 .byte 0 msl0Velocity .byte <mslVelocity .byte >mslVelocity msl1Velocity .byte 255 - <mslVelocity .byte 255 - >mslVelocity ballVelocityX ; (HHLL assembled to LLHH) .word $0100 .word $0180 .word $FF00 .word $FE80 ballVelocityY ; (HHLL assembled to LLHH) .word $0080 .word $0100 .word $FE80 .word $FF00 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; 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 ; pfLoop hex 85 1f ; 00 sta ENABL ; draw ball hex b9 00 00 ; 02 lda 00,Y ; draw missile 1 (addr at $03) hex 85 1e ; 05 sta ENAM1 hex b9 00 00 ; 07 lda 00,Y ; draw player 0 (addr at $08) hex 85 1b ; 0A sta GRP0 hex b9 00 00 ; 0C lda 00,Y ; draw missile 0 (addr at $0D) hex 85 1d ; 0F sta ENAM0 hex b9 00 00 ; 11 lda 00,Y ; draw player 1 (addr at $12) hex 85 1c ; 14 sta GRP1 ; barrier ; alternating barrier animation hex a9 00 ; 16 lda #0 ; (pointer for start at $17) hex 49 01 ; 18 eor #1 ; the two barriers will be out of sync, because hex 85 0e ; 1A sta PF1 ; at this point we already missed PF1 at the left. hex 85 17 ; 1C sta barrier+1 ; store pattern with D0 flipped (self-modifying) hex b9 00 00 ; 1E lda 00,Y ; load ball for next line (addr at $1F) hex 88 ; 21 dey hex 85 02 ; 22 sta WSYNC hex d0 da ; 24 bne pfLoop ; start over 3 cycles into the scan line hex 4c ; 26 jmp PFEnd ; 38 + 2 bytes in total ($28) ; subroutine to move it to RAM PfReturnLoc = PFEnd - PFStart + PFRoutine relocatePFRoutine ldx #PFEnd-PFStart mvCode lda PFStart,X sta PFRoutine,X dex bpl mvCode lda #<BottomBorder ; fix up return vector sta PfReturnLoc lda #>BottomBorder sta PfReturnLoc + 1 lda #PFRoutine + $17 ; fix up the self-modifying rewrite addr sta PFRoutine + $1d 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 9: Scores!
◀ Previous: Episode 7: Let's Save Some Bytes!
▲ Back to the index.
April 2018, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2018/04. —