ZX Spectrum Assembly, Pong – 0x06 Field, paddle, ball and timing
In this ZX Spectrum Assembly chapter, we will paint the field, the paddles, the ball and timing.
Translation by Felipe Monge Corbalán
Table of contents
Field, paddle, ball and timing
We create the folder Step06 and copy the files game.asm, sprite.asm and video.asm from the folder Step05, copy controls.asm from the folder Step03 and create the file main.asm.
We will start by editing the main.asm file to set the loading position, set the border to black, clean the screen, paint the centre line and make an infinite loop to avoid returning to Basic.
We will also include the rest of the files and tell PASMO where to call when loading the program:
org $8000
Main:
ld a, $00
out ($fe), a
call Cls
call PrintLine
Loop:
jr Loop
include "controls.asm"
include "game.asm"
include "sprite.asm"
include "video.asm"
end $8000
We compile and have a look at the result in the emulator.
The next step is to paint the border of the field. In sprite.asm we add a new constant at the beginning:
FILL: EQU $ff
The routine that paints the border is implemented before the PrintLine routine, in the video.asm file:
PrintBorder:
ld hl, $4100
ld de, $56e0
ld b, $20
ld a, FILL
We load into HL the address of third 0, line 0, scanline 1, LD HL, $4100, we load into DE the address of third 2, line 7, scanline 6, LD DE, $56E0, and into B the thirty-two columns in which we will draw the border, LD B, $20. We load the border sprite into A, LD A, FILL.
We implement the loop to draw the border:
printBorder_loop:
ld (hl), a
ld (de), a
inc l
inc e
djnz printBorder_loop
ret
We paint the border sprite in the direction that HL, LD (HL), A, points to, and do the same with the direction that DE, LD (DE), A, points to.
We point HL to the next column, INC L, the same with DE, INC E. We repeat until B is zero, DJNZ printBorder_loop, after which we exit the routine, RET.
The final appearance of the PrintBorder routine is as follows:
;--------------------------------------------------------------------
; Paint the edge of the field.
; Alters the value of AD, B, DE and HL registers.
;--------------------------------------------------------------------
PrintBorder:
ld hl, $4100 ; HL = third 0, line 0, scanline 1
ld de, $56e0 ; DE = third 2, line 7, scanline 6
ld b, $20 ; B = 32 to be painted
ld a, FILL ; Load the byte to be painted into A
printBorder_loop:
ld (hl), a ; Paints direction pointed by HL
ld (de), a ; Paints address pointed by DE
inc l ; HL = next column
inc e ; DE = next column
djnz printBorder_loop ; Loop until B reaches 0
ret
To test this routine, we go back to main.asm and after the call to PrintLine, we insert the call to the new routine:
call PrintBorder
We compile and see the results in the emulator. We can see that we have drawn the field where the action will take place.
We are going to introduce the ball into our field. As we will be moving and painting it all the time, we will place the calls inside the loop, between Loop and JR Loop:
call MoveBall
call PrintBall
We compile and see the results in the emulator:
If we look at the result, we can see two problems: the ball is blurring the centre line and the edge, and it is moving at a devilish speed.
The first thing we are going to address is the speed of the ball.
In the previous step we set a HALT to wait for the screen to refresh, but this makes it move too slowly. To slow down the speed of the ball, we are going to make it not move every time it goes through the loop; it will move one out of every N times.
We continue in main.asm and, before END $8000, we declare the variable in which we will keep track of the number of times through the loop:
countLoopBall: db $00
And now let’s implement the part after the Loop tag where it checks if the ball has passed enough times for us to move it:
ld a, (countLoopBall)
inc a
ld (countLoopBall), a
cp $0f
jr nz, loop_continue
We load into A the counter of the number of times we have passed through the loop without moving the ball, LD A, (countLoopBall), increment it, INC A, and store it in memory, LD (countLoopBall), A.
We compare if the counter has passed the number of times necessary to move the ball, CP $0F, and if not, JR NZ, loop_continue, we jump.
The loop_continue tag is new and we put it just above the PrintBall call:
loop_continue:
There is one last thing to do. If the counter has reached the number of times needed to move the ball, we need to reset the counter to zero after moving the ball, otherwise we would have to wait another 255 times instead of the number of times we set.
We add the following lines after the MoveBall call and before the loop_continue tag:
ld a, ZERO
ld (countLoopBall), a
Implementing main.asm would look like the following:
org $8000
Main:
ld a, $00
out ($fe), a ; Black border
call Cls ; Clean screen
call PrintLine ; Paint centre line
call PrintBorder ; Paint border
Loop:
ld a, (countLoopBall) ; A = count ball
inc a ; Increases counter
ld (countLoopBall), a ; Loads value into memory
cp $0f ; Counter = 15?
jr nz, loop_continue ; No, skip
call MoveBall ; Move ball position
ld a, ZERO ; A = 0
ld (countLoopBall), a ; Load memory value
loop_continue:
call PrintBall ; Paint ball
jr Loop ; Infinite loop
include "game.asm"
include "controls.asm"
include "sprite.asm"
include "video.asm"
countLoopBall: db $00 ; Counter to move ball
end $8000
We compile, load into the emulator and watch the ball move at a more acceptable speed.
We have not defined a constant to compare with the ball counter as the speed will be variable in the future.
Next, we will tackle the problem of the parts that the ball erases as it passes, starting with the centre line.
As a first technique, we will repaint the part of the line that coincides with the Y-coordinate of the ball, whether the ball passes over it or not; this is unnecessary, but it helps us to time it.
We open the video.asm file and implement it after the PrintLine routine:
ReprintLine:
ld hl, (ballPos)
ld a, l
and $e0
or $10
ld l, a
We load the position of the ball into HL, LD HL, (ballPos), the line and the column into A, LD A, L, we keep the line, AND $E0, set the column to sixteen, where the vertical line is, OR $10, and load the value into L, LD L, A.
We are going to repaint six scanlines, which are the same as the ones on the sphere:
ld b, $06
reprintLine_loop:
ld a, h
We load the number of scanlines to be painted in B, LD B, $06, and the third and final scanline in A, LD A, H.
To paint the line, we painted in white in scanlines 0 and 7, and the visible part of the line in the rest:
and $07
cp $01
jr c, reprintLine_00
cp $07
jr z, reprintLine_00
We keep the scanline, AND $07, and check if it is one, CP $01. If it is less than one we jump, JR C, reprintLine_00, otherwise we check if it is seven, CP $07. If the scanline is equals to seven, we jump, JR Z, reprintLine_00.
If we have not jumped, the scanline is between one and six:
ld c, LINE
jr reprintLine_loopCont
We load the line sprite in C, LD C, LINE, and jump, JR reprintLine_loopCont.
If we jumped before, the scanline is zero or seven:
reprintLine_00:
ld c, ZERO
We load the empty sprite in C, LD C, ZERO, and paint accordingly:
reprintLine_loopCont:
ld a, (hl)
or c
ld (hl), a
call NextScan
djnz reprintLine_loop
ret
We load the address value of the byte to be repainted into A, LD A, (HL), add the pixels of the line to be repainted, OR C, and paint it on the screen, LD (HL), A. We calculate the address of the next scanline, CALL NextScan, repeat the operation until B is zero, DJNZ reprintLine_loop and exit, RET.
The final aspect of the routine is as follows:
;--------------------------------------------------------------------
; Repaint the centre line.
; Alters the value of the AF, BC and HL registers.
;--------------------------------------------------------------------
ReprintLine:
ld hl, (ballPos) ; HL = ball position
ld a, l ; A = row and column
and $e0 ; A = line
or $10 ; A = row and column 16 ($10)
ld l, a ; HL = initial position repaint
ld b, $06 ; Repaints 6 scanlines
reprintLine_loop:
ld a, h ; A = third and scanline
and $07 ; A = scanline
; If it is on scanline 0 or 7 it paints ZERO
; If you are on scanline 1 to 6 paint LINE
cp $01 ; Scanline = 1?
jr c, reprintLine_00 ; Scanline < 1, paint $00
cp $07 ; Scanline = 7?
jr z, reprintLine_00 ; Scanline = 7, paints ZERO
ld c, LINE ; Scanline from 1 to 6, paint LINE
jr reprintLine_loopCont ; Skip
reprintLine_00:
ld c, ZERO ; Scanline 0 or 7, paints ZERO
reprintLine_loopCont:
ld a, (hl) ; A = pixels current position
or c ; A = A OR C (adds pixels from C)
ld (hl), a ; Paints current position result
call NextScan ; Next Scanline
djnz reprintLine_loop ; Until B = 0
ret
We save four bytes and nineteen clock cycles by modifying six lines of the routine. It’s up to you, and we’ll see how to do it in step 10.
Now we just need to test what we have implemented by opening Main.asm and after the call to PrintBall we add the call to ReprintLine:
call ReprintLine
We compile and see the results in the emulator:
The centre line is no longer blurred, but we notice that the speed of the ball has decreased. We must remember that we are now performing many more operations than before. As we go on, we will adjust the speed of the ball.
We now want to prevent the border from being erased, so we will change the upper and lower limits of the ball; they are in sprite.asm:
BALL_BOTTOM: EQU $b8
BALL_TOP: EQU $02
Compile, load in the emulator and check that the border is no longer erased.
Now let’s start with the paddles. Go back to main.asm and add the following lines between CALL ReprintLine and JR Loop:
ld hl, (paddle1pos)
call PrintPaddle
ld hl, (paddle2pos)
call PrintPaddle
We load the position of the first paddle, paddle one, in HL, LD HL, (paddle1pos), and print it, CALL PrintPaddle. We do the same with paddle two.
Notice that the paddles are painted in all iterations of the loop, as well as the ball and the line repainting.
We compile and see the results in the emulator:
The paddles are drawn and the ball does not erase them as it passes. You can also see that the ball is now much slower, because we are doing more operations in each iteration of the loop.
To make the ball move faster again, let’s change the value in main.asm that the counter had to reach for the ball to move:
ld (countLoopBall), a
cp $06 ; Change!
jr nz, loop_continue
We compile, load the emulator and check that the ball is moving faster again.
Let’s implement the routine to move the paddles; we already saw how to do this in step three. We edit the file game.asm and go to the end of it.
The routine we are going to implement receives the control keystrokes in D:
MovePaddle:
bit $00, d
jr z, movePaddle_1Down
We check if player one’s up key was pressed, BIT $00, D. If not, we jump, JR Z, movePaddle_1Down, to check if the down key was pressed.
If not, player one’s up key has been pressed:
ld hl, (paddle1pos)
ld a, PADDLE_TOP
call CheckTop
jr z, movePaddle_2Up
Next we load the position of paddle one in HL, LD HL, (paddle1pos), the upper limit for paddles in register A, LD A, PADDLE_TOP, and check if it has been reached, CALL CheckTop. If the limit has been reached, we move on to check the controls for player two, JR Z, movePaddle_2Up.
If the upper limit has not been reached, we move paddle one:
call PreviousScan
ld (paddle1pos), hl
jr movePaddle_2Up
Now we need to calculate the new position of paddle one, CALL PreviousScan, load it into memory, LD (paddle1pos), HL, and jump to check the keystrokes of player two’s controls, JR movePaddle_2Up.
If player one’s up button has not been pressed, we check that the down button has been pressed:
movePaddle_1Down:
bit $01, d
jr z, movePaddle_2Up
We check whether player one’s down key, BIT $01, D, has been pressed. If it has not been pressed, we jump to check the keystrokes of player two’s controls, JR Z, movePaddle_2Up.
If it does not jump, player two’s down key has been pressed:
ld hl, (paddle1pos)
ld a, PADDLE_BOTTOM
call CheckBottom
jr z, movePaddle_2Up
We load the position of paddle one into the HL register, LD HL, (paddle1pos), the lower limit for the paddles into the A register, LD A, PADDLE_BOTTOM, and check if the limit has been reached, CALL CheckBottom. If it has been reached, we jump to checking the controls for player two, JR Z, movePaddle_2Up.
If the lower limit has not been reached, move paddle one:
call NextScan
ld (paddle1pos), hl
We calculate the new position for paddle one, CALL NextScan, and load it into memory, LD (paddle1pos), HL.
We make the checks using the controls for player two. Given the similarity, we simply mark the changes with respect to player one’s check:
movePaddle_2Up: ; Change!
bit $02, d ; Change!
jr z, movePaddle_2Down ; Change!
ld hl, (paddle2pos) ; Change!
ld a, PADDLE_TOP
call CheckTop
jr z, movePaddle_End ; Change!
call PreviousScan
ld (paddle2pos), hl ; Change!
jr movePaddle_End ; Change!
movePaddle_2Down: ; Change!
bit $03, d ; Change!
jr z, movePaddle_End ; Change!
ld hl, (paddle2pos) ; Change!
ld a, PADDLE_BOTTOM
call CheckBottom
jr z, movePaddle_End ; Change!
call NextScan
ld (paddle2pos), hl ; Change!
movePaddle_End:
ret
The final aspect of the routine is as follows:
;--------------------------------------------------------------------
; Calculate the position of the paddles to move them.
; Input: D -> Controls keystrokes
; Alters the value of the AF and HL registers.
;--------------------------------------------------------------------
MovePaddle:
bit $00, d ; A pressed?
jr z, movePaddle_1Down ; Not pressed, skip
ld hl, (paddle1pos) ; HL = paddle position 1
ld a, PADDLE_TOP ; A = top margin
call CheckTop ; Reached top margin?
jr z, movePaddle_2Up ; Reached, skip
call PreviousScan ; Scanline previous to position paddle 1
ld (paddle1pos), hl ; Load new position paddle 1 into memory
jr movePaddle_2Up ; Jump
movePaddle_1Down:
bit $01, d ; Z pressed?
jr z, movePaddle_2Up ; Not pressed, skip
ld hl, (paddle1pos) ; HL = paddle position 1
ld a, PADDLE_BOTTOM ; A = bottom margin
call CheckBottom ; Reached bottom margin?
jr z, movePaddle_2Up ; Reached, skip
call NextScan ; Scanline next to position paddle 1
ld (paddle1pos), hl ; Load new position paddle 1 into memory
movePaddle_2Up:
bit $02, d ; 0 pressed?
jr z, movePaddle_2Down ; Not pressed, skip
ld hl, (paddle2pos) ; HL = paddle position 2
ld a, PADDLE_TOP ; A = top margin
call CheckTop ; Reached top margin?
jr z, movePaddle_End ; Reached, skip
call PreviousScan ; Scanline previous to position paddle 2
ld (paddle2pos), hl ; Loads new paddle position 2 to memory
jr movePaddle_End ; Jump
movePaddle_2Down:
bit $03, d ; O pressed?
jr z, movePaddle_End ; Not pressed, skip
ld hl, (paddle2pos) ; HL = paddle position 2
ld a, PADDLE_BOTTOM ; A = bottom margin
call CheckBottom ; Reached bottom margin?
jr z, movePaddle_End ; Reached, skip
call NextScan ; Scanline next to position paddle 2
ld (paddle2pos), hl ; Loads new paddle position 2 to memory
movePaddle_End:
ret
We can save two bytes and two clock cycles, in the same way as in the previous chapter. This time we will not give the solution, as it is similar to the one in the previous chapter.
Finally, let’s implement the calls to this routine in main.asm, inside our infinite loop, just above the loop_continue tag:
loop_paddle:
call ScanKeys
call MovePaddle
First we check which keys have been pressed, CALL ScanKeys, and then we move the paddles, CALL MovePaddle.
We also need to change the label to the one that jumps when the ball does not move, four lines above:
cp $06
jr nz, loop_paddle ; Change!
call MoveBall
We compile and test in the emulator:
We noticed two problems:
- The paddles erase the edge.
- The paddles move very fast and are difficult to control.
To solve the first problem, we are going to modify the constants that mark the upper and lower limits of the paddles, which are in sprite.asm:
PADDLE_BOTTOM: EQU $a6
PADDLE_TOP: EQU $02
Compile, load into the emulator and check that the border is no longer erased:
To slow down the speed of the paddle movement, we are going to use the same technique we used with the ball, we are not going to move the paddles in every iteration of the loop.
The first thing we need to do is declare the variable we are going to use as a counter, which we do in main.asm before END $8000:
countLoopPaddle: db $00
Now, just below the loop_paddle tag, we implement the counter check:
ld a, (countLoopPaddle)
inc a
ld (countLoopPaddle), a
cp $02
jr nz, loop_continue
We load the counter into A, LD A, (countLoopPaddle), increment it, INC A, and load the value of the result into memory, LD (countLoopPaddle), A. We evaluate if the number of times we defined to move the paddles has passed, CP $02, and if not we jump, JR NZ, loop_continue.
If not, we check that no key has been pressed, CALL ScanKeys, and we move the paddles, CALL MovePaddle, and as with the ball, we must reset the counter to zero. Finally, we add the following lines before the loop_continue tag:
ld a, ZERO
ld (countLoopPaddle), a
We load zero into A, LD A, ZERO, and load it into memory, LD (countLoopPaddle), A, setting the counter to zero.
Compile and load into the emulator. Now the paddle control is less fast and more precise.
The final main.asm code is as follows:
; Field painting, paddle and ball movement and timing
org $8000
;--------------------------------------------------------------------
; Programme entry
;--------------------------------------------------------------------
Main:
ld a, $00 ; A = 0
out ($fe), a ; Black border
call Cls ; Clean screen
call PrintLine ; Prints centre line
call PrintBorder ; Print field border
Loop:
ld a, (countLoopBall) ; A = countLoopsBall
inc a ; It increases it
ld (countLoopBall), a ; Load to memory
cp $06 ; Counter = 6?
jr nz, loop_paddle ; Counter != 6, skip
call MoveBall ; Move ball
ld a, ZERO ; A = 0
ld (countLoopBall), a ; Counter = 0
loop_paddle:
ld a, (countLoopPaddle) ; A = count number of paddle turns
inc a ; It increases it
ld (countLoopPaddle), a ; Load To memory
cp $02 ; Counter = 2?
jr nz, loop_continue ; Counter != 2, skip
call ScanKeys ; Scan for keystrokes
call MovePaddle ; Move paddles
ld a, ZERO ; A = 0
ld (countLoopPaddle), a ; Counter = 0
loop_continue:
call PrintBall ; Paint ball
call ReprintLine ; Reprint line
ld hl, (paddle1pos) ; HL = paddle position 1
call PrintPaddle ; Paint Paddle 1
ld hl, (paddle2pos) ; HL = paddle position 2
call PrintPaddle ; Paint Paddle 2
jr Loop ; Infinite loop
include "game.asm"
include "controls.asm"
include "sprite.asm"
include "video.asm"
countLoopBall: db $00 ; Counter turns ball
countLoopPaddle: db $00 ; Counter turns paddles
end $8000
ZX Spectrum Assembly, Pong
In the next ZX Spectrum Assembly chapter, we will implement collision detection.
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.