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.
Two game states.
- 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. —