Espamática
ZX SpectrumRetroZ80 Assembly

ZX Spectrum Assembly, Pong – 0x09 Change of direction and ball speed

In this 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.

Translation by Felipe Monge Corbalán

Table of contents

Change of direction and ball speed

We are going to dispense with some of what we implemented in the previous step. The speed of the ball will change depending on which part of the paddle it collides with.

Create a folder called Step09. Copy controls.asm, game.asm, main.asm, sprite.asm and video.asm from Step08.

First we will remove the ability to change the speed of the ball with keys 1 to 3.

Open controls.asm and in the ScanKeys routine delete all lines up to the scanKeys_ctrl tag, leaving the beginning of the routine as follows:

ASM
ScanKeys:
ld   d, $00

scanKeys_A:

When we compile and load in the emulator, we notice that the speed of the ball does not change.

We will add constants and variables to sprite.asm to control the inclination of the ball. We will also change the sprites of the paddles, which will draw the four pixels closest to the centre of the screen.

We add the constants that specify the rotation that the ball should have when it collides with the paddle:

ASM
CROSS_LEFT_ROT:  EQU $ff
CROSS_RIGHT_ROT: EQU $01

We add the initial position of the ball and the cumulative number of moves the ball must make to change the Y position. We will use the latter data to change the tilt of the ball:

ASM
BALLPOS_INI:  EQU $4850
ballMovCount: db $00

We change the initial configuration of the ball and the documentation (comments) of the ball:

ASM
; Ball speed and direction.
; bits 0 to 3:  Movements of the ball to change the Y position. 
;               Values: f = half-diagonal, 2 = half-diagonal, 1 = diagonal
; bits 4 and 5: Ball speed:  1 very fast, 2 fast, 3 slow
; bit 6:        X-address:   0 right / 1 left
; bit 7:        Y-direction: 0 up / 1 down
ballSetting:   db $31      ; 0011 0001

According to the new configuration, the ball initially moves to the right and upwards, at a slow speed, and the Y-position changes with each movement.

We add the paddle sprites and remove the previous one:

ASM
; PADDLE:        EQU$3c
PADDLE1:       EQU $0f
PADDLE2:       EQU $f0

Finally, we add the initial positions of the paddles:

ASM
PADDLE1POS_INI:EQU $4861
PADDLE2POS_INI:EQU $487e

We have added separate sprites for each paddle and removed the constant we used to paint the paddles; when we compile, we get errors.

We fix these errors by modifying the PrintPaddle routine in video.asm.

The PrintPaddle routine receives the position of the paddle in HL. Now it also gets the sprite of the paddle in C.

We modify the line below the printPaddle_loop tag:

ASM
ld   (hl), PADDLE

We leave it as follows:

ASM
ld   (hl), c

We compile it, it doesn’t give an error, but when we load it into the emulator, we see that the results are not what we want.

The paddle being painted by the sprite we defined. This is because we have not loaded in C which sprite to paint.

Open main.asm and look for the loop_continue tag. From line five we print the paddles, load the HL with the position of the paddle and call the painting of the paddle. Before calling the painting method we need to specify the sprite to paint.

This is what it looks like after the modification:

ASM
ld   hl, (paddle1pos)
ld   c, PADDLE1           ; Change!
call PrintPaddle
ld   hl, (paddle2pos)
ld   c, PADDLE2           ; Change!
call PrintPaddle

We compile, open in the emulator and see that the paddles are well painted again.

Taking advantage of the fact that we are in main.asm, we are going to change a behaviour that you may not have noticed. At the end of one game, and the start of another, the paddles stay in the same position they were at the end of the game, and the ball goes out of the court of the player who scored the last point.

We add the following lines before the Loop tag:

ASM
ld   hl, BALLPOS_INI
ld   (ballPos), hl
ld   hl, PADDLE1POS_INI
ld   (paddle1pos), hl
ld   hl, PADDLE2POS_INI
ld   (paddle2pos), hl

The ball and the paddles are placed in their initial positions.

When we compile, we get an error:

ERROR on line 68 of file main.asm
ERROR: Relative jump out of range

This error occurs because when we add lines, we have some JR that is out of range. JR can only jump 127 bytes forwards or 128 bytes backwards, and we have some JR that is jumping to an address outside this range.

Specifically, we have two JR Z, Main and one JR Loop at the end of the main.asm file. We replace these three JR with JP and we have solved the problem. JP occupies one byte more than JR, our program has grown by three bytes, but we have reduced six clock cycles.

We compile the game, load it into the emulator and see that when we stop the game and start another one, both the ball and the paddles return to their initial position.

We have implemented the change in speed, inclination and direction of the ball when it collides with the paddles.

Open game.asm and look for the checkBallCross_left tag. We find it three lines above:

ASM
ld   a, $ff

We modify this line and leave it as follows:

ASM
ld   a, CROSS_LEFT_ROT

We look for the CheckCrossX tag and three lines above it:

ASM
ld   a, $01

We change this line and leave it as follows:

ASM
ld   a, CROSS_RIGHT_ROT

We have changed the values to constants so that if we need to change the values in the future, we will be able to locate them more easily.

The next step is to change the ball configuration, which depends on the collision zone.

We divide the paddle into five. The behaviour depends on the collision zone:

Strike zoneVertical directionInclinationSpeed
1/5UpDiagonal3 (slow)
2/5UpSemi-diagonal2 (normal)
3/5It does not changeSemi-flat1 (fast)
4/5BelowSemi-diagonal2 (normal)
5/5BelowDiagonal3 (slow)
ZX Spectrum Assembly, Pong

We locate the CheckCrossY tag, go to the penultimate line, XOR A, and implement before it:

ASM
ld   a, c	
sub  $15
ld   c, a	
ld   a, b
add  a, $04
ld   b, a

At this point we have in C the position of the penultimate scanline of the paddle and in B the position of the ball. Both positions are in TTLLLSSS format.

We load into A the position of the penultimate scanline of the paddle, LD A, C, we position ourselves in the first scanline, SUB $15, and we load the value back into C, LD C, A.

We load the position of the ball into A, LD A, B, we position ourselves at the bottom of the ball, ADD A, $04, and we load the value back into B, LD B, A.

From here we implement the change of behaviour depending on the collision position of the ball:

ASM
checkCrossY_1_5:
ld   a, c
add  a, $04
cp   b
jr   c, checkCrossY_2_5

We load the vertical position of the paddle in A, LD A, C, position ourselves on the last scanline of the first part, ADD A, $04, and compare it with the position of the ball, CP B.

If there is carry, the ball is lower; we jump to check the next part, JR C, checkCrossY_2_5. If there is no carry, the ball has collided in this part and we must change its configuration:

ASM
ld   a, (ballSetting)	
and  $40
or   $31
jr   checkCrossY_end

We load the ball configuration in A, LD A, (ballSetting), leave the horizontal direction (it comes calculated), AND $40, and set the vertical direction up, speed three and diagonal tilt, OR $31. We jump to the end of the routine, JR checkCrossY_end.

If the ball has not collided with the first part, we check if it has collided with the second part:

ASM
checkCrossY_2_5:
ld   a, c
add  a, $09
cp   b
jr   c, checkCrossY_3_5

We load the vertical position of the paddle in A, LD A, C, position ourselves on the last scanline of the second part, ADD A, $09, and compare it with the position of the ball, CP B. If there is carry, the ball is lower and we jump to check the next part, JR C, checkCrossY_3_5.

If there is no carry, the ball has collided in this part and we must change its configuration:

ASM
ld   a, (ballSetting)
and  $40
or   $22
jr   checkCrossY_end

We load the ball setting in A, LD A, (ballSetting), we keep the horizontal direction (it comes calculated), AND $40, and we set the vertical direction up, speed two and semi-diagonal tilt, OR $22. We jump to the end of the routine, JR checkCrossY_end.

If the ball has not collided with the second part, we check if it has collided with the third part:

ASM
checkCrossY_3_5:
ld   a, c
add  a, $0d
cp   b
jr   c, checkCrossY_4_5

We load the vertical position of the paddle in A, LD A, C, go to the last scanline of the third part, ADD A, $0D, and compare it with the position of the ball, CP B. If there is a carry, the ball goes lower and we jump to check the next part, JR C, checkCrossY_4_5.

If there is no carry, the ball has collided in this part and we need to change its configuration:

ASM
ld   a, (ballSetting)	
and  $c0	
or   $1f	
jr   checkCrossY_end

We load the ball configuration in A, LD A, (ballSetting), keep the horizontal and vertical directions (which are calculated), AND $C0, and set the speed to one and semi-flat tilt, OR $1F. We jump to the end of the routine, JR checkCrossY_end.

If the non-ball collided with the third part, we check if it collided with the fourth part:

ASM
checkCrossY_4_5:
ld   a, c
add  a, $11
cp   b
jr   c, checkCrossY_5_5

We load the vertical position of the paddle in A, LD A, C, go to the last scanline of the fourth part, ADD A, $11, and compare it with the position of the ball, CP B. If there is carry, the ball is lower and we jump to check the next part, JR C, checkCrossY_5_5.

If there is no carry, the ball has collided in this part and we need to change its configuration:

ASM
ld   a, (ballSetting)
and  $40
or   $a2
jr   checkCrossY_end

We load the ball configuration in A, LD A, (ballSetting), we keep the horizontal direction (it comes calculated), AND $40, and we set the vertical direction down, speed two and semi-diagonal tilt, OR $A2. We jump to the end of the routine, JR checkCrossY_end.

If the ball has not collided with the fourth part of the paddle, it has collided with the fifth part:

ASM
checkCrossY_5_5:
ld   a, (ballSetting)	
and  $40
or   $b1

We load the ball configuration in A, LD A, (ballSetting), we keep the horizontal direction (it is calculated), AND $40, and we set the vertical direction, speed three and the diagonal inclination, OR $B1.

Finally, just above XOR A, we add the end-of-function tag we mentioned and load the new ball configuration into memory:

ASM
checkCrossY_end:
ld   (ballSetting), a

After XOR A, let’s set the ball movement counter to 0:

ASM
ld   (ballMovCount), a

The final aspect of the routine is as follows:

ASM
; -------------------------------------------------------------------
; Evaluates whether the ball collides in the Y-axis with the paddle.
; In the event of a collision, update the ball configuration.
; Input:  HL -> Paddle position	
; Output: Z  -> Collide.
;         NZ -> No collision.
; Alters the value of the AF, BC and HL registers.
; -------------------------------------------------------------------
CheckCrossY:
call GetPtrY               ; Vertical position paddle (TTLLLLSSS)
; The position points to the first scanline of the paddle which is at 0
inc  a                     ; A = next scanline
ld   c, a                  ; C = A
ld   hl, (ballPos)         ; HL = ball position
call GetPtrY               ; Vertical position ball (TTLLLSSS)
ld   b, a                  ; B = A
; Check if the ball goes over the paddle
; The ball is composed of 1 scanline at 0, 4 at $3c and another at 0
; Position points to 1st scanline, check for collision with 5th scanline
add  a, $04                ; A = 5th scanline
sub  c                     ; A = ball position - paddle position
ret  c                     ; Carry? Out, ball passes over
; Check if the ball passes under the paddle
ld   a, c                  ; A = vertical position paddle
add  a, $16                ; A = penultimate scanline, last != 0
ld   c, a                  ; C = A
ld   a, b                  ; A = vertical position ball
inc  a                     ; A = 1st scanline, first != 0
sub  c                     ; A = ball position - paddle position
ret  nc                    ; Carry? No, ball passes underneath 
                           ; or collide in the last scanline
                           ; The latter case activates flag Z

; Depending on collision location, inclination and speed
ld   a, c                  ; A = penultimate scanline of paddle
sub  $15                   ; A = first scanline
ld   c, a                  ; C = A

ld   a, b                  ; A = ball position
add  a, $04                ; A = lower ball
ld   b, a                  ; B = A

checkCrossY_1_5:
ld   a, c                  ; A = vertical position paddle
add  a, $04                ; A = last scanline of 1/5
cp   b                     ; Compare with ball position
jr   c, checkCrossY_2_5    ; Carry? Ball is lower, jump
ld   a, (ballSetting)      ; A = ball configuration
and  $40                   ; A = horizontal direction
or   $31                   ; up, speed 3 and diagonal
jr   checkCrossY_end       ; End of routine

checkCrossY_2_5:
ld   a, c                  ; A = vertical position paddel
add  a, $09                ; A = last scanline of 2/5
cp   b                     ; Compare with ball position
jr   c, checkCrossY_3_5    ; Carry? Ball is lower, jump
ld   a, (ballSetting)      ; A = ball setting 
and  $40                   ; A = horizontal direction
or   $22                   ; up, speed 2 and semi-diagonal
jr   checkCrossY_end       ; End of routine

checkCrossY_3_5:
ld   a, c                  ; A = vertical position paddle
add  a, $0d                ; A = last scanline of 3/5
cp   b                     ; Compare with ball position
jr   c, checkCrossY_4_5    ; Carry? Ball is lower, jump
ld   a, (ballSetting)      ; A = ball configuration
and  $c0                   ; A = horizontal and vertical direction
or   $1f                   ; speed 1 and semi flat
jr   checkCrossY_end       ; End of routine

checkCrossY_4_5:
ld   a, c                  ; A = vertical position paddle
add  a, $11                ; A = last scanline of 4/5
cp   b                     ; Compare with ball position
jr   c, checkCrossY_5_5    ; Carry? Ball is lower, jump
ld   a, (ballSetting)      ; A = ball configuration
and  $40                   ; A = horizontal and vertical direction
or   $a2                   ; down, speed 2 and semi-diagonal
jr   checkCrossY_end       ; End of routine

checkCrossY_5_5:
ld   a, (ballSetting)      ; A = ball configuration
and  $40                   ; A = horizontal direction
or   $b1                   ; down, speed 3 and diagonal

; There is a collision
checkCrossY_end:
ld   (ballSetting), a      ; Load into memory ball configuration
xor  a                     ; Flag Z = 1, A = 0
ld   (ballMovCount), a     ; Ball movement counter = 0
ret

We compile, load into the emulator and look at the results.

We see that the speed changes depending on the collision zone of the ball, but not the inclination. Also, when a goal is scored, the speed is not reset, which makes it very difficult to continue playing when the ball is at maximum speed.

Why does the speed change but not the slope?

If we think back, in the previous step we implemented the possibility of changing the speed of the ball with keys 1 to 3. In fact, we started this step by warning that we were going to abandon this implementation, but what we did not abandon is the change we made in Main.asm to take into account the speed of the ball in the configuration; that is why the speed changes.

What is missing is the implementation to take into account the tilt, so that when a point is scored, the ball speed and tilt are reset.

We will start by changing the tilt. We continue in the file game.asm and implement the routine that changes the Y-position of the ball; we implement it after the RET of the moveBall_end tag:

ASM
MoveBallY:
ld   a, (ballSetting)
and  $0f
ld   d, a

We load the ball setting into A, LD A, (ballSetting), keep the slope, AND $0F, and load the value into D, LD A, D.

ASM
ld   a, (ballMovCount)
inc  a
ld   (ballMovCount), a
cp   d
ret  nz

We load the number of moves the ball makes into A, LD A, (ballMovCount), increment it, INC A, load the value into memory, LD (ballMovCount), A, and compare it with D, which contains the number of moves needed to change the Y position, CP D. If they are not equal, it has not been reached and we exit, RET NZ.

ASM
xor  a
ld   (ballMovCount), a
ret

When we have reached the value, we set A = 0 and activate the Z flag, XOR A, zero the accumulated movements of the ball, LD (ballMovCount), A, and exit, RET. Activating the Z flag indicates to the caller that the Y position of the ball needs to be changed.

The final aspect of the routine is as follows:

ASM
; -------------------------------------------------------------------
; Changes the Y position of the ball
; Alters the value of the AF and D registers.
; -------------------------------------------------------------------
MoveBallY:
ld   a, (ballSetting)      ; A = ball configuration
and  $0f                   ; A = inclination
ld   d, a                  ; D = A

ld   a, (ballMovCount)     ; A = accumulated ball movements
inc  a                     ; A = A + 1
ld   (ballMovCount), a     ; Loads value into memory
cp   d                     ; Compare with inclination
ret  nz                    ; Different? Exit, no change of position

; The position must change
xor  a                     ; A = 0, flag Z = 1
ld   (ballMovCount), a     ; Accumulated ball movements = 0

ret

Locate the moveBall_up tag and add the following between the JR Z, moveBall_upChg and CALL PreviousScan lines:

ASM
call MoveBallY
jr   nz, moveBall_x

We check if the Y position of the ball needs to be changed, CALL MoveBallY, and if not it jumps, JR NZ, moveBall_x.

Locate the moveBall_down tag and add the following between the lines JR Z, moveBall_downChg and CALL NextScan:

ASM
call MoveBallY
jr   nz, moveBall_x

We check if the Y-position of the ball needs to be changed, CALL MoveBallY, and if not it jumps, JR NZ, moveBall_x.

We compile, load the emulator and check that the tilt and speed change.

Finally, we will make it so that when a point is scored, the speed and inclination of the ball are reset.

We locate the SetBallLeft routine, remove the AND $BF line and replace it with the following:

ASM
; and  $bf     ; Delete!
and  $80
or   $31

We keep the Y direction, AND $80, set the horizontal direction to the right, speed three and diagonal tilt, OR $31.

Before the RET command, we add the following lines:

ASM
ld   a, $00
ld   (ballMovCount), a

We set A to zero, LD A, $00, and the movements of the ball, LD (ballMovCount), A.

We locate the SetBallRight routine, remove the OR $40 line and replace it with the following:

ASM
; or   $40     ; Delete!
and  $80
or   $71

We keep the Y direction, AND $80, set the horizontal direction to the left, the speed to three and the diagonal tilt, OR $11.

Before the RET command, we add the following lines:

ASM
ld   a, $00
ld   (ballMovCount), a

We set A to zero, LD A, $00, and the movements of the ball, LD (ballMovCount), A.

The final appearance of both 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         ; Load value into memory
ld   a, $01                ; A = 1
ld   (ballRotation), a     ; Rotation = 1
ld   a, (ballSetting)      ; A direction and velocity ball
and  $80                   ; A = Y-direction
or   $31                   ; X right, speed 3 and diagonal
ld   (ballSetting), a      ; New ball address in memory
ld   a, $00                ; A = 0
ld   (ballMovCount), a     ; Ball movement counter = 0

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         ; Load value into memory
ld   a, $ff                ; A = -1
ld   (ballRotation), a     ; Rotation = -1
ld   a, (ballSetting)      ; A = ball direction and velocity
and  $80                   ; A = Y-direction
or   $71                   ; X left, speed 3 and diagonal
ld   (ballSetting), a      ; New ball address in memory
ld   a, $00                ; A = 0
ld   (ballMovCount), a     ; Ball movement counter = 0

ret

We compile, load the emulator and see the results, which should be as expected, although the ball is a bit slow, isn’t it?

Have you noticed that when the ball hits the bottom of the paddle it doesn’t change its vertical direction, pitch or speed? In step ten we will see why.

ZX Spectrum Assembly, Pong

In the next ZX Spectrum Assembly chapter, we will implement several optimisations.

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