ZX Spectrum Assembly, Pong – 0x04 We start moving the ball
In this ZX Spectrum Assembly chapter, we will make the first tests to move the ball.
Translation by Felipe Monge Corbalán
Table of contents
We start moving the ball
Create a folder called Step04, create a file called main.asm in it and copy sprite.asm and video.asm from Step03.
We start by editing the sprite.asm file to define the necessary ball data:
BALL_BOTTOM: EQU $ba
BALL_TOP: EQU $00
As with the paddles, we define lower and upper limits for the ball, in the format TTLLLSSS.
ballPos: dw $4870
ballSetting: db $00
ballRotation: db $f8
As with the paddles, we will use a variable to hold the position of the ball at each moment, ballPos.
In ballSetting we store the X-speed in bits 0 to 3, the Y-speed in bits 4 and 5, the X-direction (0 right / 1 left) in bit 6 and the Y-direction (0 up / 1 down) in bit 7.
We store the rotation of the ball in ballRotation, where rotation to the right is represented by positive values and rotation to the left by negative values.
The rotation is necessary because of the way we are going to do the horizontal movement.
The ball will consist of one empty scanline, four scanlines with the visible part and another empty scanline. The blank scanlines ensure that the ball leaves no trace when it moves.
We will define two bytes to paint the ball and define each movement pixel by pixel:
; Ball sprite: 1 line at 0, 4 lines visible, 1 line at 0
ballRight: ; Right Sprite Left
db $3c, $00 ; +0/$00 00111100 00000000 -8/$f8
db $1e, $00 ; +1/$01 00011110 00000000 -7/$f9
db $0f, $00 ; +2/$02 00001111 00000000 -6/$fa
db $07, $80 ; +3/$03 00000111 10000000 -5/$fb
db $03, $c0 ; +4/$04 00000011 11000000 -4/$fc
db $01, $e0 ; +5/$05 00000001 11100000 -3/$fd
db $00, $f0 ; +6/$06 00000000 11110000 -2/$fe
db $00, $78 ; +7/$07 00000000 01111000 -1/$ff
ballLeft:
db $00, $3c ; +8/$08 00000000 00111100 +0/$00
Each line defines the visible part of the sphere, depending on how the pixels are positioned. We define two bytes per position. In the comment you can see the rotation when the ball goes to the right, the bytes we are going to paint, and the rotation when the ball goes to the left.
The start ball is drawn as shown in the first sprite:
00111100 00000000
If it moves one pixel to the right, we don’t change the position of the ball, we change the rotation and draw the second sprite:
00011110 00000000
When we reach the last rotation, we change the position of the ball, specifically the column. The last aspect of the code is:
; Limits of the objects on the screen
BALL_BOTTOM: EQU $ba ; TTLLLSSS
BALL_TOP: EQU $00 ; TTLLLLLSSSSS
; Ball sprite:
; 1 line at 0, 4 lines 3c, 1 line at 0
ballRight: ; Right Sprite Left
db $3c, $00 ; +0/$00 00111100 00000000 -8/$f8
db $1e, $00 ; +1/$01 00011110 00000000 -7/$f9
db $0f, $00 ; +2/$02 00001111 00000000 -6/$fa
db $07, $80 ; +3/$03 00000111 10000000 -5/$fb
db $03, $c0 ; +4/$04 00000011 11000000 -4/$fc
db $01, $e0 ; +5/$05 00000001 11100000 -3/$fd
db $00, $f0 ; +6/$06 00000000 11110000 -2/$fe
db $00, $78 ; +7/$07 00000000 01111000 -1/$ff
ballLeft:
db $00, $3c ; +8/$08 00000000 00111100 +0/$00
; Ball position
ballPos: dw $4870 ; 010T TSSS LLLC CCCC
; Ball speed and direction.
; bits 0 to 3: speed X: 1 to 4
; bits 4 to 5: speed Y: 0 to 3
; bit 6: X direction: 0 right / 1 left
; bit 7: Y direction: 0 up / 1 down
ballSetting: db $00
; Ball rotation
; Positive values right, negative values left
ballRotation: db $f8
Now we are going to implement the routine that paints the ball in video.asm; we are going to put it after the PreviousScan routine:
PrintBall:
ld b, $00
ld a, (ballRotation)
ld c, a
cp $00
ld a, $00
jp p, printBall_right
First we find out the horizontal direction of the ball. Then we add or subtract the rotation to the base sprite of the ball to get the sprite to paint. The base direction of the sprite is stored in HL and we subtract or add the rotation that we will have in BC, so the first thing to do is set B to zero, LD B, $00.
The next step is to load the rotation of the ball into A, LD A, (ballRotation), and from there into C, LD C, A. We could load the value directly into C, after passing through HL, but depending on the value we get, it will go to the right or to the left. To get this, we compare the value with zero, and since the comparisons are always made against the A register, it is necessary to load the rotation into this register.
We compare the value of A with zero, CP A, $00, if the result is positive, the ball moves to the right and we jump, JP P, printBall_right. Before, we set A = 0 for the following calculations, LD A, $00.
We continue to implement the movement to the left:
printBall_left:
ld hl, ballLeft
sub c
add a, a
ld c, a
sbc hl, bc
jr printBall_continue
If the ball moves to the left, we have to load the direction of the left base sprite in HL, LD HL, ballLeft.
At this point A is zero, so we subtract the rotation we have in C to get the value we need to subtract to get the correct sprite, SUB C:
Example: C = $FF, A = $00 A – C = $01
As each sprite occupies two bytes, we have to duplicate the value to subtract from HL, ADD A, A, and then load it into C, LD C, A.
Now we can calculate the address of the sprite to print, SBC HL, BC, and print the ball, JR printBall_continue.
We implement the movement to the right:
printBall_right:
ld hl, ballRight
add a, c
add a, a
ld c, a
add hl, bc
If the ball moves to the right, the routine is slightly different. We again load the direction of the base sprite into HL, LD HL, ballRight, in this case it goes to the right, we add the rotation to A, ADD A, C, multiply by two, ADD A, A, and load the result into C, LD C, A, and then add it to HL, ADD HL, BC, to get the direction of the sprite to print.
And now we print the ball:
printBall_continue:
ex de, hl
ld hl, (ballPos)
Since the NextScan routine receives the current address in HL and returns, also in HL, the new address, the first thing to do is to load the value of HL in DE, EX DE, HL. With EX we exchange the value of the registers and save four clock cycles and one byte compared to LD (LD D, H and LD E, L). We load the position of the ball in HL, LD HL, (ballPos).
ld (hl), ZERO
inc l
ld (hl), ZERO
dec l
call NextScan
We paint the first byte of the first scanline empty, LD (HL), ZERO, increment the column to go to the next byte, INC L, paint the second byte, LD (HL), ZERO, leave the column as it was, DEC L, and get the next scanline, CALL NextScan.
The next step is to draw the four scanlines that are actually visible on the ball:
ld b, $04
printBall_loop:
ld a, (de)
ld (hl), a
inc de
inc l
ld a, (de)
ld (hl), a
dec de
dec l
call NextScan
djnz printBall_loop
We load the scanlines to be painted into B, LD B, $04, the first byte of the sprite into A, LD A, (DE), and paint it, LD (HL), A.
We point DE to the next byte of the sprite, INC DE, point HL to the next column, INC L, load the sprite into A, LD A, (DE), and paint it, LD (HL), A.
We repoint DE to the first byte of the sprite, DEC DE, HL, to the previous column, DEC L, and calculate the next scanline, CALL NextScan.
Repeat all operations until B is zero, DJNZ printBall_loop.
ld (hl), ZERO
inc l
ld (hl), ZERO
ret
We draw the first byte of the last empty ball scanline, LD (HL), ZERO, increment L to point to the next column, INC L, and draw the second byte, LD (HL), ZERO.
The final code of the routine is as follows:
;--------------------------------------------------------------------
; Paint the ball.
; Alters the value of the AF, BC, DE and HL registers.
;--------------------------------------------------------------------
PrintBall:
ld b, $00 ; B = 0
ld a, (ballRotation) ; A = ball rotation, what to paint?
ld c, a ; C = A
cp $00 ; Compare with 0, see where it rotates to
ld a, $00 ; A = 0
jp p, printBall_right ; If positive jumps, rotates to right
printBall_left:
; The rotation of the ball is to the left
ld hl, ballLeft ; HL = address bytes ball
sub c ; A = A-C, ball rotation
add a, a ; A = A+A, ball = two bytes
ld c, a ; C = A
sbc hl, bc ; HL = HL-BC (ball offset)
jr printBall_continue
printBall_right:
; Ball rotation is clockwise
ld hl, ballRight ; HL = address bytes ball
add a, c ; A = A+C, ball rotation
add a, a ; A = A+A, ball = two bytes
ld c, a ; C = A
add hl, bc ; HL = HL+BC (ball offset)
printBall_continue:
; The address of the ball definition is loaded in DE.
ex de, hl
ld hl, (ballPos) ; HL = ball position
; Paint the first line in white
ld (hl), ZERO ; Moves target to screen position
inc l ; L = next column
ld (hl), ZERO ; Moves target to screen position
dec l ; L = previous column
call NextScan ; Next scanline
ld b, $04 ; Paint ball in next 4 scanlines
printBall_loop:
ld a, (de) ; A = byte 1 definition ball
ld (hl), a ; Load ball definition on screen
inc de ; DE = next byte definition ball
inc l ; L = next column
ld a, (de) ; A = byte 2 definition ball
ld (hl), a ; Load ball definition on screen
dec de ; DE = first byte definition ball
dec l ; L = previous column
call NextScan ; Next scanline
djnz printBall_loop ; Until B = 0
; Paint the last blank line
ld (hl), ZERO ; Moves target to screen position
inc l ; L = next column
ld (hl), ZERO ; Moves target to screen position
ret
Now we just need to see if everything we implemented works, for which we will edit main.asm:
org $8000
ld a, $02
out ($fe), a
ld a, $00
ld (ballRotation), a
We specify the address to load the program, ORG $8000, set A = 2, LD A, $02, to set the border to red, OUT ($FE), A, and then set A = 0, LD A, $00, to initialise the ball rotation, LD (ballRotation), A.
We implement an infinite loop so that the ball moves indefinitely:
Loop:
call PrintBall
We are going to print the ball, CALL PrintBall, in the home position:
loop_cont:
ld b, $08
loopRight:
exx
ld a, (ballRotation)
inc a
ld (ballRotation), a
call PrintBall
exx
halt
djnz loopRight
Let’s move, rotate, the ball eight pixels to the right, LD B, $08, swapping the values with the alternate registers to preserve the value of B, EXX.
EXX swaps the value of the general purpose registers with the value of the alternate registers:
BC <-> ‘BC
DE <-> ‘DE
HL <-> ‘HL
We have chosen EXX in this case because it takes four clock cycles and occupies one byte, while PUSH BC takes eleven clock cycles and the value of the registers, except for B, is not critical for any operation to be performed in the loop, and we see this instruction.
We load the rotation of the ball into A, LD A, (ballRotation), increment the rotation, INC A, and load the resulting value into memory, LD (ballRotation), A.
We psint the ball, CALL PrintBall, exchange the values of the registers, EXX, and retrieve the value of B and pause to see how the ball moves, HALT.
Repeat until B is zero, DJNZ loopRight.
We set the rotation of the ball to zero, this time without painting it, to start rotating the pixels to the left (see the definition of the ball sprite):
ld a, $00
ld (ballRotation), a
We are going to move the ball eight pixels to the left. Only one instruction and one label change in relation to the shift to the right, so the routine will not be explained, we will mark the changes so you can see the difference:
ld b, $08
loopLeft: ; Change!
exx
ld a, (ballRotation)
dec a ; Change!
ld (ballRotation), a
call PrintBall
exx
halt
djnz loopLeft ; Change!
To finish, we reset the rotation to zero. We load the value into memory and repeat the loop:
ld a, $00
ld (ballRotation), a
jr loop_cont
Don’t forget to include the files sprite.asm and video.asm. Tell PASMO where to call them when loading the program:
include "sprite.asm"
include "video.asm"
end $8000
The ball does not move, on the contrary, we always draw it in the same two columns, moving the pixels eight times to the right and then eight times to the left, and we repeat this over and over again.
The final appearance of the main.asm file is as follows:
; Move the ball from left to right between two columns.
org $8000
ld a, $02 ; A = 2
out ($fe), a ; Red border
ld a, $00 ; A = 0
ld (ballRotation), a ; Rotation ball = 0
Loop:
call PrintBall ; Print ball
loop_cont:
ld b, $08 ; Move ball 8 pixels to the right
loopRight:
exx ; Exchanges records, preserves B
ld a, (ballRotation) ; A = ball rotation
inc a ; Increases turnover
ld (ballRotation), a ; Store rotation value
call PrintBall ; Print ball
exx ; Exchanges records, retrieves B
halt ; Synchronise with screen refresh
djnz loopRight ; Until B = 0
ld a, $00 ; A = 0
ld (ballRotation), a ; Rotation ball = 0
ld b, $08 ; Move ball 8 pixels to the right
loopLeft:
exx ; Exchanges records, preserves B
ld a, (ballRotation) ; A = ball rotation
dec a ; Decreases the rotation
ld (ballRotation), a ; Store rotation value
call PrintBall ; Print ball
exx ; Exchanges records, retrieves B
halt ; Synchronise with screen refresh
djnz loopLeft ; Until B = 0
ld a, $00 ; A = 0
ld (ballRotation), a ; Rotation ball = 0
jr loop_cont ; Infinite loop
include "sprite.asm"
include "video.asm"
end $8000
All that’s left is to compile and see the results in the emulator:
ZX Spectrum Assembly, Pong
In the next ZX Spectrum Assembly chapter, we will move the ball around the screen.
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.