Espamática
ZX SpectrumRetroZ80 Assembly

ZX Spectrum Assembly, Pong – 0x05 We move the ball around the screen

In this ZX Spectrum Assembly chapter, we will move the ball around the screen.

We move the ball around the screen

Create the folder Step05, create main.asm and game.asm, and copy sprite.asm and video.asm from the folder Step04.

We edit sprite.asm to add the two counters we need to move the ball around the screen:

MARGIN_LEFT:   EQU $00
MARGIN_RIGHT:  EQU $1e

Just as we have vertical and horizontal boundaries, we need right and left boundaries to keep the ball within them.

The next step is to implement the logic of the ball movement, which we will do in game.asm:

MoveBall:
ld   a, (ballSetting)	
and  $80
jr   nz, moveBall_down

First we load the current ball configuration into A, LD A, (ballSetting), and hold bit 7, AND $80, which tells us whether the ball is moving up or down. If the bit is not set to zero, the ball moves down and jumps, JR NZ, moveBall_down.

If the bit is set to zero, the ball moves up:

moveBall_up:
ld   hl, (ballPos)
ld   a, BALL_TOP
call CheckTop
jr   z, moveBall_upChg
call PreviousScan
ld   (ballPos), hl
jr   moveBall_x

We load the current position of the ball in HL, LD HL, (ballPos), the vertical limit in A, LD A, BALL_TOP and check if the limit has been reached, CALL CheckTop. If the Z flag is set, the limit has been reached and a jump is made to change the vertical direction of the ball, JR Z, moveBall_upChg.

If we have not reached the vertical limit, we must get the previous scanline, CALL PreviousScan, load it into memory, LD (ballPos), HL, and jump to check the horizontal offset, JR moveBall_x.

If it reaches the limit, we change the vertical direction of the ball:

moveBall_upChg:
ld   a, (ballSetting)
or   $80
ld   (ballSetting), a
call NextScan
ld   (ballPos), hl
jr   moveBall_x

We load the ball setting into A, LD A, (ballSetting), set bit 7, OR $80, to indicate that the ball should go down, and load the value into memory, LD (ballSetting), A. We need to calculate the new vertical position of the ball, CALL NextScan, load the value into memory, LD (ballPos), HL, and jump to check for horizontal displacement, JR moveBall_x.

To activate bit 7, we have done an OR with $80 (10000000). It is useful to remember the result of the OR operation depending on the value of the bits:

Bit 1Bit 2Result
000
101
011
111
ZX Spectrum Assembly, Pong

As you can see in the table, using OR $80 sets bit 7 to 1 and leaves the rest as they are.

If the ball went down at the start of the routine, do something similar to what we have seen above:

moveBall_down:
ld   hl, (ballPos)
ld   a, BALL_BOTTOM
call CheckBottom
jr   z, moveBall_downChg
call NextScan
ld   (ballPos), hl
jr   moveBall_x

We load the position of the ball in HL, LD HL, (ballPos), and the lower limit in A, LD A, BALL_BOTTOM, check if we have reached it, CALL CheckBottom, in which case we jump to change the direction of the ball, JR Z, moveBall_downChg.

If we have not reached the bottom, we must calculate the new position of the ball, CALL NextScan, load it into memory, LD (ballPos), HL, and jump to check the horizontal displacement, JR moveBall_x.

If the lower limit is reached, the vertical direction of the ball must be changed:

moveBall_downChg:
ld   a, (ballSetting)
and  $7f
ld   (ballSetting), a	
call PreviousScan
ld   (ballPos), hl

We load the ball setting into A, LD A, (ballSetting), disable bit 7, AND $7F, to indicate that the ball should now go up, and load this value into memory, LD (ballSetting), A. We calculate the new vertical position of the ball, CALL PreviousScan, and load the value into memory, LD (ballPos), HL.

To disable bit 7 we have done an AND with $7F, in binary it is 01111111. It is useful to remember the result of the AND operation depending on the value of the bits:

Bit 1Bit 2Result
000
100
010
111
ZX Spectrum Assembly, Pong

As you can see in the table, applying AND $7F sets bit 7 to zero and leaves the rest as they are.

We start to calculate the horizontal shift:

moveBall_x:
ld   a, (ballSetting)
and  $40
jr   nz, moveBall_left

We load the ball configuration in A, LD A, (ballSetting), check the status of bit 6, AND $40, and if it is not zero, the ball moves to the left and jumps, JR NZ, moveBall_left.

If bit 6 is zero, the ball moves to the right:

moveBall_right:
ld   a, (ballRotation)
cp   $08
jr   z, moveBall_rightLast
inc  a 
ld   (ballRotation), a
jr   moveBall_end

We load the rotation of the ball into A, LD A, (ballRotation), and check if it is the last, CP $08, in which case we must jump, JR Z, moveBall_rightLast.

If it is not in the last rotation, we increment it, INC A, load it into memory, LD (ballRotation), A, and jump to the end of the routine, JR moveBall_end.

If it is on the last rotation and not on the right border, we move the ball to the next column:

moveBall_rightLast:
ld   a, (ballPos)
and  $1f
cp   MARGIN_RIGHT
jr   z, moveBall_rightChg
ld   hl, ballPos
inc  (hl)
ld   a, $01
ld   (ballRotation), a
jr   moveBall_end

We load the row and column in A, LD A, (ballPos), leave the column, AND $1F, and check if we have reached the right margin, CP MARGIN_RIGHT, in which case we jump to change the direction of the ball, JR Z, moveBall_rightChg.

If the right limit is not reached, we move the ball to the next column. We load the address where the ball is in HL, LD HL, ballPos, and increment the column, INC (HL).

We set the ball rotation to one, LD A, $01, load it into memory, LD (ballRotation), A, and jump to the end of the routine, JR moveBall_end.

As you can see, to load the column into A, the instruction used was LD A, (ballPos) and to increment the column, LD HL, ballPos and INC (HL).

Given that the memory locations on the screen are coded as 010TTTTSSS LLLCCCCCCC, do we load and modify the scanline? No, because the Z80 is a Little Endian microcontroller.

A Little Endian microcontroller, when loading 16-bit values into memory, loads the least significant byte into the first memory location and the most significant byte into the next, so if memory location $C000 loads the value $4000, $C000 loads $00 and $C001 loads $40. Therefore, when loading the value from (ballPos) into A, the least significant byte is loaded, which is where the row and column are. Incrementing (HL) increments the column.

If the load is done on a 16-bit register, it loads the low byte into the low part of the register and the high byte into the high part. That’s why when ballPos is loaded in HL, it loads the high byte of the memory address in H and the low byte in L.

When it has reached the right limit, the direction of the ball must be changed:

moveBall_rightChg:
ld   a, (ballSetting)
or   $40
ld   (ballSetting), a	
ld   a, $ff
ld   (ballRotation), a
jr   moveBall_end

We load the ball setting into A, LD A, (ballSetting), set bit 6 to change the direction to the left, OR $40, and load the value into memory, LD (ballSetting), A.

We set the rotation to minus one, LD A, $FF, load it into memory, LD (ballRotation), A, and jump to the end, JR moveBall_end.

If the ball moves to the left, you need to do something similar to what we saw above:

moveBall_left:
ld   a, (ballRotation)
cp   $f8
jr   z, moveBall_leftLast
dec  a 
ld   (ballRotation), a
jr   moveBall_end

We load the rotation into the A register, LD A, (ballRotation), check if it is the last, CP $F8, and jump if it is, JR Z, moveBall_leftLast.

If we are not at the last rotation, we decrement it, DEC A, load the value into memory, LD (ballRotation), A, and jump to the end, JR moveBall_end.

If we have reached the last rotation but not the left border, we move the ball to the previous column:

moveBall_leftLast:
ld   a, (ballPos)
and  $1f
cp   MARGIN_LEFT
jr   z, moveBall_leftChg
ld   hl, ballPos
dec  (hl)
ld   a, $ff
ld   (ballRotation), a
jr   moveBall_end

We load the row and column in A, LD A, (ballPos), keep the column, AND $1F, and check if it has reached the left margin, CP MARGIN_LEFT, in which case we must jump, JR Z, moveBall_leftChg.

If it has not reached the left margin, we load the address where the ball position is in HL, LD HL, ballPos, and decrement the column, DEC (HL).

We set the rotation to minus one, LD A, $FF, load the value into memory, LD (ballRotation), A, and jump to the end, JR moveBall_end.

Finally, we change direction when the left boundary is reached:

moveBall_leftChg:
ld   a, $01
ld   (ballRotation), a
ld   a, (ballSetting)
and  $bf
ld   (ballSetting), a

moveBall_end:
ret

We set the ball rotation to one, LD A, $01, and load it into memory, LD (ballRotation), A. We load the ball setting into A, LD A, (ballSetting), set the direction to right by turning off bit 6, AND $BF, and load the value we have obtained into memory, LD (ballSetting), A.

We can save two clock cycles and five bytes by making a small modification. It’s up to you; we’ll see how to do it in step 10.

The final aspect of the routine is as follows:

;--------------------------------------------------------------------
; Calculates the position, rotation and direction of the ball
; to paint it.
; Alters the value of the AF and HL registers.
;--------------------------------------------------------------------
MoveBall:
ld   a, (ballSetting)      ; A = ball direction and ball speed
and  $80                   ; Check vertical direction
jr   nz, moveBall_down     ; bit 7 = 1?, goes down

moveBall_up:
; Ball goes up
ld   hl, (ballPos)         ; HL = ball position
ld   a, BALL_TOP           ; A = upper margin
call CheckTop              ; Reached top margin?
jr   z, moveBall_upChg     ; If reached, jumps
call PreviousScan          ; Scanline previous to ball position
ld   (ballPos), hl         ; Loads new ball position into memory
jr   moveBall_x            ; Jump

moveBall_upChg:
; Ball goes up, has reached the stop and changes direction
ld   a, (ballSetting)      ; A = ball direction and velocity
or   $80                   ; Vertical direction = down
ld   (ballSetting), a      ; Load new address ball into memory
call NextScan              ; Scanline next to ball position
ld   (ballPos), hl         ; Loads new ball position into memory
jr   moveBall_x            ; Jump

moveBall_down:
; Ball goes down
ld   hl, (ballPos)         ; HL = ball position
ld   a, BALL_BOTTOM        ; A = upper margin
call CheckBottom           ; Reached upper margin?
jr   z, moveBall_downChg   ; If reached jumps
call NextScan              ; Scanline next to ball position
ld   (ballPos), hl         ; Loads new ball position into memory
jr   moveBall_x            ; Jump

moveBall_downChg:
; Ball goes down, has reached the stop and changes direction
ld   a, (ballSetting)      ; A = ball direction and ball velocity
and  $7f                   ; Vertical direction = up
ld   (ballSetting), a      ; Load new address ball into memory
call PreviousScan          ; Scanline previous to ball position
ld   (ballPos), hl         ; Loads new ball position into memory

moveBall_x:
ld   a, (ballSetting)      ; A = ball direction and ball velocity
and  $40                   ; Check horizontal direction
jr   nz, moveBall_left     ; bit 6 = one? goes to the left

moveBall_right:
; Ball goes to the right
ld   a, (ballRotation)     ; A = ball rotation
cp   $08                   ; Last rotation?
jr   z, moveBall_rightLast ; If last rotation skip
inc  a                     ; Increases turnover 
ld   (ballRotation), a     ; Loading into memory
jr   moveBall_end          ; End of routine

moveBall_rightLast:
; He is in the last rotation
; If you have not reached the right limit, set the rotation to 1
; and puts the ball in the next column
ld   a, (ballPos)          ; A = line and column ball
and  $1f                   ; Remains with column
cp   MARGIN_RIGHT          ; Buy with right boundary
jr   z, moveBall_rightChg  ; Reached, skip
ld   hl, ballPos           ; HL = ball position
inc  (hl)                  ; Increments column
ld   a, $01                ; Set rotation to 1
ld   (ballRotation), a     ; Load value into memory
jr   moveBall_end          ; End of routine

moveBall_rightChg:
; You have reached the right limit
; Set rotation to -1 and change horizontal direction of ball
ld   a, (ballSetting)      ; A = ball direction and ball speed
or   $40                   ; Horizontal direction = left
ld   (ballSetting), a      ; Loads new ball address into memory
ld   a, $ff                ; A = -1
ld   (ballRotation), a     ; Loads it into memory Rotation = -1
jr   moveBall_end          ; End of routine

moveBall_left:
; Ball goes to the left
ld   a, (ballRotation)     ; A = current ball rotation
cp   $f8                   ; Last rotation?
jr   z, moveBall_leftLast  ; If last rotation skip
dec  a                     ; Decreasing rotation 
ld   (ballRotation), a     ; Loading into memory
jr   moveBall_end          ; End of routine

moveBall_leftLast:
; He is in the last rotation
; If you have not reached the left limit, set the rotation to -1
; and puts the ball in the previous column
ld   a, (ballPos)          ; A = row and column
and  $1f                   ; It remains only with column
cp   MARGIN_LEFT           ; Left boundary?
jr   z, moveBall_leftChg   ; If it has reached it, it jumps
ld   hl, ballPos           ; HL = ball position address
dec  (hl)                  ; Goes to previous column
ld   a, $ff                ; Set rotation to -1
ld   (ballRotation), a     ; Load value into memory
jr   moveBall_end          ; End of routine

moveBall_leftChg:
; You have reached the left limit
; Set rotation to 1 and change direction
ld   a, $01                ; HL = ball position
ld   (ballRotation), a     ; Load value into memory Rotation = 1
ld   a, (ballSetting)      ; A = ball direction and ball velocity
and  $bf                   ; Sets horizontal right direction
ld   (ballSetting), a      ; Load new memory ball address

moveBall_end:
ret

Now it’s time to test the implementation. We edit main.asm; the implementation is very simple:

org  $8000

ld   a, $02
out  ($fe), a
call PrintBall

We specify the address where the programme is to be loaded, set the border to red and draw the ball in its initial position.

Loop:
call MoveBall
call PrintBall
halt
jr   Loop

We implement an infinite loop in which we move the ball, paint it, wait for the screen to refresh, and repeat these three operations indefinitely.

include "game.asm"
include "sprite.asm"
include "video.asm"

end  $8000

We include the necessary files and tell PASMO where to call them when loading the programme.

The final appearance of main.asm is as follows:

; Move the ball around the screen by tracing diagonals.
org  $8000

ld   a, $02                ; A = 2
out  ($fe), a              ; Turns border red
call PrintBall             ; Prints the ball

Loop:
call MoveBall              ; Move the ball
call PrintBall             ; Paint the ball
halt                       ; Wait for screen refresh
jr   Loop                  ; Infinite loop

include "game.asm"
include "sprite.asm"
include "video.asm"

end  $8000

The big moment is here, we compile and have a look at the result in the emulator:

ZX Spectrum Assembly, Pong
ZX Spectrum Assembly, Pong

Does the ball seem slow to you? Comment on the HALT.

ZX Spectrum Assembly, Pong

In the next ZX Spectrum Assembly chapter, we will paint the field, the paddles, the ball and timing.

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