Espamática
ZX SpectrumRetroZ80 Assembly

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:

ASM
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):

ASM
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:

ASM
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:

ASM
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:

ASM
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:

ASM
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:

ASM
;--------------------------------------------------------------------
; 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:

ASM
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
ZX Spectrum Assembly, Pong

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:

ASM
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:

ASM
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.

ASM
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).

ASM
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:

ASM
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):

ASM
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.

ASM
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:

ASM
pop  hl
pop  de
ret

This is the final aspect of the marker painting routine:

ASM
;--------------------------------------------------------------------
; 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:

ASM
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:

ASM
;--------------------------------------------------------------------
; 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:

ASM
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:

ASM
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:

ASM
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.

ASM
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:

ASM
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:

ASM
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:

ASM
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:

ASM
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).

ASM
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:

ASM
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:

ASM
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:

ASM
;--------------------------------------------------------------------
; 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:

ASM
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:

ASM
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:

ASM
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:

ASM
;--------------------------------------------------------------------
; 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:

ASM
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:

ASM
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:

ASM
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:

ASM
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:

ASM
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.

ASM
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:

ASM
;--------------------------------------------------------------------
; 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:

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:

ASM
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:

ASM
;--------------------------------------------------------------------
; 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:

ASM
call ClearBall
call SetBallLeft

The final aspect of the routine is as follows:

ASM
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:

ASM
call ClearBall
call SetBallRight

The final aspect of the routine is as follows:

ASM
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.

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.



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies

Descubre más desde Espamática

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo