Espamática
ZX SpectrumRetroZ80 Assembly

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:

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

ASM
FILL:	       EQU $ff

The routine that paints the border is implemented before the PrintLine routine, in the video.asm file:

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

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

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

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

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

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

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

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

ASM
ld   a, ZERO
ld   (countLoopBall), a

Implementing main.asm would look like the following:

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

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

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

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

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

ASM
reprintLine_00:
ld   c, ZERO

We load the empty sprite in C, LD C, ZERO, and paint accordingly:

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

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

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

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:

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

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

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

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

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

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

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

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

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

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

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

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

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:

ASM
countLoopPaddle: db $00

Now, just below the loop_paddle tag, we implement the counter check:

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

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

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

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