ZX Spectrum Assembly, Pong – 0x08 Two players game
In this ZX Spectrum Assembly chapter, we will implement the two player game with a scoreboard and the ability to change the speed of the ball.
Translation by Felipe Monge Corbalán
Table of contents
Two players game
Create the folder Step08 and copy the files controls.asm, game.asm, main.asm, sprite.asm and video.asm from the folder Step07.
We start by defining the position where we want to draw the score and the sprites we need in the sprite.asm file:
POINTS_P1: EQU $450d
POINTS_P2: EQU $4511
Each digit in the markers occupies 8×16 pixels, one character wide by two characters high (1 byte x 16 bytes/scanlines):
White_sprite:
ds $10 ; 16 spaces = 16 bytes at $00
Zero_sprite:
db $00, $7e, $7e, $66, $66, $66, $66, $66
db $66, $66, $66, $66, $66, $7e, $7e, $00
One_sprite:
db $00, $18, $18, $18, $18, $18, $18, $18
db $18, $18, $18, $18, $18, $18, $18, $00
Two_sprite:
db $00, $7e, $7e, $06, $06, $06, $06, $7e
db $7e, $60, $60, $60, $60, $7e, $7e, $00
Three_sprite:
db $00, $7e, $7e, $06, $06, $06, $06, $3e
db $3e, $06, $06, $06, $06, $7e, $7e, $00
Four_sprite:
db $00, $66, $66, $66, $66, $66, $66, $7e
db $7e, $06, $06, $06, $06, $06, $06, $00
Five_sprite:
db $00, $7e, $7e, $60, $60, $60, $60, $7e
db $7e, $06, $06, $06, $06, $7e, $7e, $00
Six_sprite:
db $00, $7e, $7e, $60, $60, $60, $60, $7e
db $7e, $66, $66, $66, $66, $7e, $7e, $00
Seven_sprite:
db $00, $7e, $7e, $06, $06, $06, $06, $06
db $06, $06, $06, $06, $06, $06, $06, $00
Eight_sprite:
db $00, $7e, $7e, $66, $66, $66, $66, $7e
db $7e, $66, $66, $66, $66, $7e, $7e, $00
Nine_sprite:
db $00, $7e, $7e, $66, $66, $66, $66, $7e
db $7e, $06, $06, $06, $06, $7e, $7e, $00
With the sprites ready, we define the numbers so that they refer to the sprite labels:
Zero:
dw White_sprite, Zero_sprite
One:
dw White_sprite, One_sprite
Two:
dw White_sprite, Two_sprite
Three:
dw White_sprite, Three_sprite
Four:
dw White_sprite, Four_sprite
Five:
dw White_sprite, Five_sprite
Six:
dw White_sprite, Six_sprite
Seven:
dw White_sprite, Seven_sprite
Eight:
dw White_sprite, Eight_sprite
Nine:
dw White_sprite, Nine_sprite
Ten:
dw One_sprite, Zero_sprite
Eleven:
dw One_sprite, One_sprite
Twelve:
dw One_sprite, Two_sprite
Thirteen:
dw One_sprite, Three_sprite
Fourteen:
dw One_sprite, Four_sprite
Fifteen:
dw One_sprite, Five_sprite
We need to define where the players’ scores will be stored. We open main.asm and add two tags before END $8000:
p1points: db $00
p2points: db $00
We are now ready to start implementing the scoreboard.
We need to know which sprite to paint depending on the score. To know this, we will implement a routine that receives the score in A and returns the address of the sprite to paint in HL.
We open video.asm to implement the routine just before the NextScan routine:
GetPointSprite:
ld hl, Zero
ld bc, $04
inc a
We load the address of the zero sprite into HL, LD HL, Zero. As each sprite is four bytes away from the previous one, we load this offset into BC, LD BC, $04, and increment A so that the loop does not start at zero, INC A, to prevent it from malfunctioning if the points are zero.
Now we loop HL to point to the correct sprite:
getPointSprite_loop:
dec a
ret z
add hl, bc
jr getPointSprite_loop
We decrement A, DEC A, and if we have reached zero, HL already points to the correct sprite and we exit, RET Z. If we have not yet reached zero, we add the offset to HL, ADD HL, BC, and repeat the loop, JR getPointSprite_loop.
This is the final aspect of the routine:
;--------------------------------------------------------------------
; Gets the corresponding sprite to paint on the marker.
; Input: A -> score.
; Output: HL -> address of the sprite to be painted.
; Alters the value of the AF, BC and HL registers.
;--------------------------------------------------------------------
GetPointSprite:
ld hl, Zero ; HL = address sprite 0
ld bc, $04 ; Sprite is 4 bytes away from
; the previous one
inc a ; Increment A, loop start != 0
getPointSprite_loop:
dec a ; Decreasing A
ret z ; A = 0, end of routine
add hl, bc ; Add 4 to sprite address
jr getPointSprite_loop ; Loop until A = 0
Now let’s implement the routine that draws the markers at the end of the video.asm file:
PrintPoints:
ld a, (p1points)
call GetPointSprite
We load the points of player one into A, LD A, (p1points) and get the memory address of the sprite definition corresponding to this score, CALL GetPointSprite.
GetPointSprite returns in HL the address where the sprite was defined. If the score is zero, HL will give us the memory address where the zero tag is defined, i.e:
Zero:
dw White_sprite, Zero_sprite
As we can see, zero is defined by two other memory addresses: the first is the address where the empty sprite used to justify two digits is defined, and the second is the address where the zero sprite is defined.
If the memory addresses were as follows:
$9000 White_sprite
$9020 Zero_sprite
$9040 Zero
The definition of the Zero tag, after replacing the White_sprite and Zero_sprite tags with the memory addresses where they are defined, would be as follows:
Zero:
Dw $9000, $9020
The value of HL after calling GetPointSprite when the marker is zero would be $9040, i.e. the memory address defining the zero marker.
As the Z80 is little-endian, the memory address values would be from $9040 onwards:
$9040 | $00 |
$9041 | $90 |
$9042 | $20 |
$9043 | $90 |
In other words, the memory addresses where the sprites for White_sprite and Zero_sprite are defined.
This explanation is necessary to understand how the rest of the routine works:
push hl
ld e, (hl)
inc hl
ld d, (hl)
ld hl, POINTS_P1
call printPoint_print
We are going to paint the first digit of the first player’s marker. We keep the value of HL that points to the sprite of the number we are going to paint, PUSH HL, load into E the low byte of the address of the sprite of the first digit, LD E, (HL), point to HL the high byte of the address, INC HL, and load it into D, LD D, (HL).
We load into HL the screen address where the first digit of player one’s marker will be printed, LD HL, POINTS_P1, and call to paint, CALL printPointPrint.
Now we paint the second digit of player one’s marker:
pop hl
inc hl
inc hl
We get the value of HL, POP HL, and point it to the bottom of the address where the second digit sprite is defined, INC HL, INC HL.
ld e, (hl)
inc hl
ld d, (hl)
We load the low part of this address into E, LD E, (HL), point HL to the high part of the address, INC HL, and load it into D, LD D, (HL).
ld hl, POINTS_P1
inc l
call printPoint_print
We load into HL the screen location where player one’s marker is painted, LD HL, POINTS_P1. As each digit is one byte wide, we place HL in the column where the second digit, INC L, is painted and paint it, CALL printPoint_print.
The way player two’s marker is painted is almost the same as player one’s, so we show the code by bolding the changes without going into detail:
ld a, (p2points) ; Change!
call GetPointSprite
push hl
; 1st digit
ld e, (hl)
inc hl
ld d, (hl)
ld hl, POINTS_P2 ; Change!
call printPoint_print
pop hl
; 2nd digit
inc hl
inc hl
ld e, (hl)
inc hl
ld d, (hl)
ld hl, POINTS_P2 ; Change!
inc l
As you can see, the changes are few. The last line has been removed because it is not necessary to call it to draw the second digit of player two, we will implement it after the last INC L.
Remember that each digit occupies 8 x 16 pixels (one column and sixteen scanlines):
printPoint_print:
ld b, $10
push de
push hl
We load into B the number of scanlines to be painted, LD B, $10, and keep the value of the DE register, PUSH DE, and of HL, PUSH HL.
printPoint_printLoop:
ld a, (de)
ld (hl), a
inc de
call NextScan
djnz printPoint_printLoop
We load into A the byte to paint, LD A, (DE), and paint it, LD (HL), A. We point DE to the next byte, INC DE, get the address of the next scanline, CALL NextScan, and repeat the operation until B is 0 and we have painted all the scanlines, DJNZ printPoint_printLoop.
Finally, we recover the values of HL and DE and exit:
pop hl
pop de
ret
This is the final aspect of the marker painting routine:
;--------------------------------------------------------------------
; Paint the scoreboard.
; Each number is 1 byte wide by 16 bytes high.
; Alters the value of the AF, BC, DE and HL registers.
;--------------------------------------------------------------------
PrintPoints:
ld a, (p1points) ; A = points player 1
call GetPointSprite ; Gets sprite to be painted in marker
push hl ; Preserves the value of HL
; 1st digit of player 1
ld e, (hl) ; HL = low part 1st digit address
; E = (HL)
inc hl ; HL = high side address 1st digit
ld d, (hl) ; D = (HL)
ld hl, POINTS_P1 ; HL = memory address where to paint
; points player 1
call printPoint_print ; Paint 1st digit marker player 1
pop hl ; Retrieves the value of HL
; 2nd digit of player 1
inc hl
inc hl ; HL = low part 2nd digit address
ld e, (hl) ; E = (HL)
inc hl ; HL = high side address 2nd digit
ld d, (hl) ; D = (HL)
ld hl, POINTS_P1 ; HL = memory address where to paint
; points player 1
inc l ; HL = address where to paint 2nd digit
call printPoint_print ; Paint 2nd digit marker player 1
ld a, (p2points) ; A = points player 2
call GetPointSprite ; Gets sprite to be painted in marker
push hl ; Preserves value of HL
; 1st digit of player 2
ld e, (hl) ; HL = low part 1st digit address
; E = (HL)
inc hl ; HL = high side address 1st digit
ld d, (hl) ; D = (HL)
ld hl, POINTS_P2 ; HL = memory address where to paint
; points player 2
call printPoint_print ; Paint 1st digit marker player 2
pop hl ; Retrieves the value of HL
; 2nd digit of player 2
inc hl
inc hl ; HL = low part 2nd digit address
ld e, (hl) ; E = (HL)
inc hl ; HL = high side address 2nd digit
ld d, (hl) ; D = (HL)
ld hl, POINTS_P2 ; HL = memory address where to paint
; points player 2
inc l ; HL = address where 2nd digit paints
; Paint the second digit of player 2's marker.
printPoint_print:
ld b, $10 ; Each digit: 1 byte x 16 (scanlines)
push de ; Preserves the value of DE
push hl ; Preserves the value of HL
printPoint_printLoop:
ld a, (de) ; A = byte to be painted
ld (hl), a ; Paints the byte
inc de ; DE = next byte
call NextScan ; HL = next scanline
djnz printPoint_printLoop ; Until B = 0
pop hl ; Retrieves the value of HL
pop de ; Retrieves the value of DE
ret
In the routine, it is easy to save twelve clock cycles and two bytes. This is done by moving two instructions around, which allows us to remove two more: we will see this in step 10.
All that remains is to see if what we have implemented works.
Open main.asm and under the call to PrintBorder, just before Loop, add the following line:
call PrintPoints
We compile and load into the emulator to see the results.
At first everything goes well, but as the ball moves we see that we have a problem, an old acquaintance of ours, and that is that the ball erases the marker as it passes; we are going to solve it below.
To prevent the ball from erasing the marker, we do the same as we did with the centre line, we repaint the marker.
We implement this routine at the end of the video.asm file.
The marker repaint routine is almost the same as the painting routine, except that we change the name of the labels and add a line. Below we show the final appearance, with the changes to PrintPoints marked:
;--------------------------------------------------------------------
; Change!
; Repaint the scoreboard.
; Each number is 1 byte wide by 16 bytes high.
; Alters the value of the AF, BC, DE and HL registers.
;--------------------------------------------------------------------
; Change!
ReprintPoints:
ld a, (p1points) ; A = points player 1
call GetPointSprite ; Gets sprite to be painted marker
push hl ; Preserves value of HL
; 1st digit of player 1
ld e, (hl) ; HL = low part 1st digit address
; E = (HL)
inc hl ; HL = high side address 1st digit
ld d, (hl) ; D = (HL)
ld hl, POINTS_P1 ; HL = memory address where to paint
; points player 1
; Change!
call reprintPoint_print ; Paint 1st digit marker player 1
pop hl ; Retrieves value of HL
; 2nd digit of player 1
inc hl
inc hl ; HL = low part 2nd digit address
ld e, (hl) ; E = (HL)
inc hl ; HL = high side address 2nd digit
ld d, (hl) ; D = (HL)
ld hl, POINTS_P1 ; HL = memory address where to paint
; points player 1
inc l ; HL = address where to paint 2nd digit
; Change!
call reprintPoint_print ; Paint 2nd digit marker player 1
ld a, (p2points) ; A = points player 2
call GetPointSprite ; Gets sprite to be painted on marker
push hl ; Preserves value of HL
; 1st digit of player 2
ld e, (hl) ; HL = low part 1st digit address
; E = (HL)
inc hl ; HL = high side address 1st digit
ld d, (hl) ; D = (HL)
ld hl, POINTS_P2 ; HL = memory address where to paint
; points player 2
; Change!
call reprintPoint_print ; Paint 1st digit marker player 2
pop hl ; Retrieves the value of HL
; 2nd digit of player 2
inc hl
inc hl ; HL = low part 2nd digit address
ld e, (hl) ; E = (HL)
inc hl ; HL = high side address 2nd digit
ld d, (hl) ; D = (HL)
ld hl, POINTS_P2 ; HL = memory address where to paint
; points player 2
inc l ; HL = address where to paint 2nd digit
; Paint the second digit of player 2's marker.
; Change!
reprintPoint_print:
ld b, $10 ; Each digit 1 byte by 16 (scanlines)
push de
push hl ; Preserves value of DE and HL
; Change!
reprintPoint_printLoop:
ld a, (de) ; A = byte to be painted
; Change!
or (hl) ; Mixes it with what is on the screen
ld (hl), a ; Paints the byte
inc de ; Points DE to the next byte
call NextScan ; Points HL to the next scanline
; Change!
djnz reprintPoint_printLoop ; Until B = 0
pop hl
pop de ; Retrieves the value of HL and DE
ret
Let us explain the line we have added:
ld a, (de)
or (hl) ; This
ld (hl), a
What we do with OR (HL) is to add the pixels on the screen to the pixels of the number sprite. This way we repaint the number without erasing the ball.
To see if it works, open main.asm and add the following line after the call to ReprintLine:
call ReprintPoints
We compile and load in the emulator to see the results.
We have solved one problem, but another has arisen. The marker no longer disappears, but the ball is very slow. Fortunately, the solution is simple, as the ball’s speed is one of the things we control.
As you may recall, the ball moves one out of every six iterations of the main loop, so all we need to do is reduce that interval in main.asm to, say, two:
ld (countLoopBall), a
cp $02 ; Change
jr nz, loop_paddle
We compile, load the emulator and check that the ball speed has increased.
Ball speed change
In the ballSetting variable we define the speed of the ball in bits 4 and 5, where one can be the fastest and three the slowest.
We will use this aspect to define and change the speed of the ball. We will go to sprite.asm and change the initial value of ballSetting.
ballSetting: db $20
This the initial value:
- Vertical direction: upwards
- Horizontal direction: right
- Speed: two
We will use this value to control the interval at which the ball moves. In main.asm we locate the Loop tag and add it at the bottom:
ld a, (ballSetting)
rrca
rrca
rrca
rrca
and $03
ld b, a
We load the ball setting in A, LD A, (ballSetting), pass the value of bits 4 and 5 to bits 0 and 1, RRCA, RRCA, RRCA, RRCA, leaving only bits 0 and 1 (ball speed), AND $03, and load the value in B, LD B, A.
Four lines down, we modify the CP $02 line:
cp b
We compile and check that everything still works the same. The difference is that now the ball speed is taken from the ball configuration and we can change it.
To change the speed of the ball we will use keys 1 to 3. Open the controls.asm file and start typing after the ScanKeys label:
scanKeys_speed:
xor a
ld (countLoopBall), a
scanKeys_ctrl:
If one of the speed change keys has been pressed, the loop counter must be reset to zero in order to paint the ball, otherwise if the counter is set to two and the speed is set to one, it will take 254 iterations for the ball to move again.
We set A to zero, XOR A, and the iteration counter for the ball to zero, LD (countLoopBall), A.
The scanKeys_ctrl tag marks the point where the routine as we have it now starts. The new implementation is between the ScanKeys and scanKeys_speed tags:
ld a, $f7
in a, ($fe)
We load the half stack 1-5 into A, LD A, $F7, and read from the keyboard port, IN A, ($FE).
bit $00, a
jr nz, scanKeys_2
We check if 1, BIT $00, A, has been pressed, and if not, we jump to check if 2, JR NZ, scanKeys_2, has been pressed.
If 1 has been pressed, we change the speed of the ball:
ld a, (ballSetting)
and $cf
or $10
ld (ballSetting), a
jr scanKeys_speed
We load the ball configuration into A, LD A, (ballSetting), set the speed bits to zero, AND $CF, set the speed to one, OR $10, load the configuration into memory, LD (ballSetting), A, and set the ball iteration counter to zero, JR scanKeys_speed.
Checking for 2 and 3 is similar to checking for 1; we look at the full code and mark the differences:
scanKeys_2:
bit $01, a ; Change!
jr nz, scanKeys_3 ; Change!
ld a, (ballSetting)
and $cf
or $20 ; Change!
ld (ballSetting), a
jr scanKeys_speed
scanKeys_3:
bit $02, a ; Change!
jr nz, scanKeys_ctrl ; Change!
ld a, (ballSetting)
and $cf
or $30 ; Change!
ld (ballSetting), a
The routine, once modified, looks like this:
;--------------------------------------------------------------------
; ScanKeys
; Scans the control keys and returns the pressed keys.
; Output: D -> Keys pressed.
; Bit 0 -> A pressed 0/1.
; Bit 1 -> Z pressed 0/1.
; Bit 2 -> 0 pressed 0/1.
; Bit 3 -> O pressed 0/1.
; Alters the value of the AF and D registers.
;--------------------------------------------------------------------
ScanKeys:
ld a, $f7 ; A = half-row 1-5
in a, ($fe) ; Reads half-stack status
bit $00, a ; 1 pressed?
jr nz, scanKeys_2 ; Not pressed, jumps
; Pressed; changes the speed of ball 1 (fast)
ld a, (ballSetting) ; A = configuration ball A
and $cf ; Set the speed bits to 0
or $10 ; Sets the speed bits to 1
ld (ballSetting), a ; Load value to memory
jr scanKeys_speed ; Jump check controls
scanKeys_2:
bit $01, a ; 2 pressed?
jr nz, scanKeys_3 ; Not pressed, skips
; Pressed; changes ball speed 2 (middle)
ld a, (ballSetting) ; A = ball configuration
and $cf ; Set the speed bits to 0
or $20 ; Sets the speed bits to 2
ld (ballSetting), a ; Load value to memory
jr scanKeys_speed ; Jump check controls
scanKeys_3:
bit $02, a ; 3 pressed?
jr nz, scanKeys_ctrl ; Not pressed, skip
; Pressed; changes the speed of the ball 3 (slow)
ld a, (ballSetting) ; A configuration = ball
or $30 ; Sets the speed bits to 3
ld (ballSetting), a ; Load value to memory
scanKeys_speed:
xor a ; A = 0
ld (countLoopBall), a ; CountLoopBall iterations = 0
scanKeys_ctrl:
ld d, $00 ; D = 0
; Rest of the routine from ScanKeys_A
We compile, load in the emulator and see how this modification behaves. If all went well, we can now change the speed of the ball.
Scoreboards
Finally, we need to count the points that the players reach, for which we will modify the MoveBall routine inside game.asm, the moveBall_rightChg and moveBall_leftChg parts.
These routines change the direction of the ball when it reaches the left or right boundary. We will implement what is needed to mark the points.
We will place the code below these markers, starting with moveBall_rightChg:
moveBall_rightChg:
ld hl, p1points
inc (hl)
call PrintPoints
We load into HL the memory address where player one’s marker is, LD HL, p1points, increment it, INC (HL), and print the marker, CALL PrintPoints. The rest of the routine stays as it was.
The modifications to the moveBall_leftChg tag are almost the same:
moveBall_leftChg:
ld hl, p2points ; Change!
inc (hl)
call PrintPoints
We compile and load into the emulator to see the results.
We have a scoreboard, but the game goes on endlessly, and when we go over fifteen points it starts to draw nonsense.
We can also see that it is getting slower and slower, but why? We paint the marker at each iteration, and to find the sprite of the number to be painted we make a loop, and a loop with fifteen iterations at most is not the same as a loop with up to two hundred and fifty-five iterations. Isn’t it? In chapter ten we will see how to implement GetPointSprite so that it always takes the same time and we save two bytes and a few clock cycles.
What we need to do now is to stop the game when one of the two players reaches fifteen points; we will also implement a way to start the game, for example by pressing 5.
At the end of controls.asm we will implement the routine that waits for the 5 to be pressed to start the game:
WaitStart:
ld a, $f7
in a, ($fe)
bit $04, a
jr nz, WaitStart
ret
We load the half stack 1-5 into A, LD A, $F7, read the keyboard, IN A, ($FE), check if 5 has been pressed, BIT $04, A, and repeat until it has been pressed, JR NZ, WaitStart.
The final aspect of the routine is as follows:
;--------------------------------------------------------------------
; WaitStart.
; Wait for the 5 key to be pressed to start the game.
; Alters the value of the AF register.
;--------------------------------------------------------------------
WaitStart:
ld a, $f7 ; A = half-row 1-5
in a, ($fe) ; Read keyboard
bit $04, a ; 5 pressed?
jr nz, WaitStart ; Not pressed, loop
ret
We go back to main.asm and after the call to PrintPoints we add the following line:
call WaitStart
We compile it, load it into the emulator and see that the game doesn’t start until we press 5.
But this is not enough, because the game does not end when one of the players reaches fifteen points.
We continue in main.asm, at the end of the loop_continue routine, just before the JR Loop; this is where we will implement the score control:
ld a, (p1points)
cp $0f
jr z, Main
We load player 1’s score into A, LD A, (p1points), compare it with fifteen, CP $0F, and if it is fifteen we jump to the start of the programme, JR Z, Main.
We do the same for player 2’s score:
ld a, (p2points) ; Change!
cp $0f
jr z, Main
We compile, load the emulator and see if any player reaches fifteen points and the game ends:
But what happens if we press 5 again? There is no way to start the game, at no point do we reset the score to zero. If we leave 5 pressed, we will see that it returns to the start and stops at each iteration of the loop.
To fix this, we go back to the beginning of main.asm and set the markers to zero just after the call to WaitStart:
ld a, ZERO
ld (p1points), a
ld (p2points), a
call PrintPoints
We set A to zero, LD A, ZERO, set player one’s score to 0, LD (p1points), A, and player two’s score, LD (p2points), A. It’s time to print the scoreboard, CALL PrintPoints. This way, when we start the game, we set the markers to zero and paint them.
We compile and load into the emulator to see the results. This is starting to take shape.
More adjustments
We still need to make some adjustments. We are going to make it so that when a goal is scored, the ball goes out on the opposite side, i.e. as if the player who scored was serving.
We will implement a routine to delete the ball, another to place it on the right side of the screen and another to place it on the left side of the screen.
The routine that erases the ball will be implemented in the video.asm file, just before the Cls routine:
ClearBall:
ld hl, (ballPos)
ld a, l
and $1f
cp $10
jr c, clearBall_continue
inc l
We load the position of the ball in HL, LD HL, (ballPos), the row and column in A, LD A, L, we keep the column, AND $1F, and compare it with the centre of the screen, CP $10.
If there is a carry, the ball can only be on the left border. We jump to clear the ball, JR C, clearBall_continue. If it does not jump, it is in the right border, but the ball is painted one column to the right (the ball is painted in two bytes/columns); we point HL to the column where the ball is painted, INC L.
clearBall_continue:
ld b, $06
clearBall_loop:
ld (hl), ZERO
call NextScan
djnz clearBall_loop
ret
We load the number of scanlines to be deleted into B, LD B, $06, delete the position to which HL points, LD (HL), ZERO, and point HL to the next scanline, CALL NextScan. Repeat until B is equals zero, DJNZ clearBall_loop, and exit, RET.
The final aspect of the routine is as follows:
;--------------------------------------------------------------------
; Delete the ball.
; Alters the value of the AF, B and HL registers.
;--------------------------------------------------------------------
ClearBall:
ld hl, (ballPos) ; HL = ball position
ld a, l ; A = row and column
and $1f ; A = column
cp $10 ; Compare with centre display
jr c, clearBall_continue ; If carry, jump, is on left
inc l ; It is in right, increase column
clearBall_continue:
ld b, $06 ; Loop 6 scanlines
clearBall_loop:
ld (hl), ZERO ; Deletes byte pointed to by HL
call NextScan ; Next scanline
djnz clearBall_loop ; Until B = 0
ret
The other two routines are implemented at the end of game.asm:
SetBallLeft:
ld hl, $4d60
ld (ballPos), hl
ld a, $01
ld (ballRotation), a
ld a, (ballSetting)
and $bf
ld (ballSetting), a
ret
We load the new ball position into HL, LD HL, $4D60, and load it into memory, LD (ballPos), HL.
We load the ball rotation into A, LD A, $01, and store it in memory, LD (ballRotation), A.
We load the ball configuration into register A, LD A, (ballSetting), set the horizontal address to the right, AND $BF, load it into memory, LD (ballSetting), A, and exit, RET.
The routine for setting the ball to the right is practically the same; we mark the differences without going into detail:
SetBallRight:
ld hl, $4d7f ; Change!
ld (ballPos), hl
ld a, $ff ; Change!
ld (ballRotation), a
ld a, (ballSetting)
or $40
ld (ballSetting), a
ret
The final appearance of the two routines is as follows:
;--------------------------------------------------------------------
; Position the ball to the left.
; Alters the value of the AF and HL registers.
;--------------------------------------------------------------------
SetBallLeft:
ld hl, $4d60 ; HL = ball position
ld (ballPos), hl ; Loads value to memory
ld a, $01 ; A = 1
ld (ballRotation), a ; Rotation = 1
ld a, (ballSetting) ; A = ball direction and ball velocity
and $bf ; A = Horizontal right direction
ld (ballSetting), a ; Horizontal direction = right
ret
;--------------------------------------------------------------------
; Position the ball to the right.
; Alters the value of the AF and HL registers.
;--------------------------------------------------------------------
SetBallRight:
ld hl, $4d7f ; HL = ball position
ld (ballPos), hl ; Loads value to memory
ld a, $ff ; A = -1
ld (ballRotation), a ; Rotation = -1
ld a, (ballSetting) ; A = ball direction and ball velocity
or $40 ; A = horizontal direction left
ld (ballSetting), a ; Horizontal direction = left
ret
To complete this step, we only need to use these routines.
We are going to modify two routines in the game.asm file: moveBall_rightChg and moveBall_leftChg.
In the moveBall_rightChg routine we need to delete the lines between CALL PrintPoints and JR moveBall_end and replace them with:
call ClearBall
call SetBallLeft
The final aspect of the routine is as follows:
moveBall_rightChg:
; You have reached the right limit, POINT!
ld hl, p1points ; HL = score address player 1
inc (hl) ; Increases score
call PrintPoints ; Paint marker
call ClearBall ; Clears ball
call SetBallLeft ; Set ball to left
jr moveBall_end ; End routine
In the moveBall_leftChg routine, we delete the lines between CALL PrintPoints and the moveBall_end tag, and replace them with:
call ClearBall
call SetBallRight
The final aspect of the routine is as follows:
moveBall_leftChg:
; You have reached the left limit, POINT!
ld hl, p2points ; HL = address score player 2
inc (hl) ; Increments marker
call PrintPoints ; Paint marker
call ClearBall ; Clears ball
call SetBallRight ; Set ball right
We compile, load the emulator and can start playing our first two-player games, although we still have a few things to do.
ZX Spectrum Assembly, Pong
In the next ZX Spectrum Assembly chapter, we will implement the change of direction and speed of the ball depending on which part of the paddle it hits.
Download the source code from here.
Useful links
ZX Spectrum Assembly, Pong by Juan Antonio Rubio García.
Translation by Felipe Monge Corbalán.
This work is licensed to Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Any comments are always welcome.