ZX Spectrum Assembly, Pong – 0x07 Collision detection
In this ZX Spectrum Assembly chapter, we will implement collision detection.
Translation by Felipe Monge Corbalán
Table of contents
Collision detection
Create the folder Step07 and copy from the folder Step06 controls.asm, game.asm, main.asm, sprite.asm and video.asm.
From now on we will use everything we have implemented and develop it further.
We are going to implement the collision detection of the ball with the paddles. To do this we need to define the column in which the collision occurs, which we will do in sprite.asm:
CROSS_LEFT: EQU $01
CROSS_RIGHT: EQU $1d
To check for collision in the X coordinate, we will use the column. To check for collision on the Y coordinate we will use the third, the line and the scanline.
As we have seen above, the Y-coordinate is in two different bytes (010T TSSS LLLC CCCC), so we implement a routine that takes a memory location from the screen and returns the Y-coordinate (TTLLLSSS).
The routine is implemented in video.asm, after the Cls routine, and receives the screen location in HL and returns the Y coordinate obtained in A:
GetPtrY:
ld a, h
and $18
rlca
rlca
rlca
ld e, a
We load the third and the scanline into A, LD A, H, keep the third, AND $18, pass it to bits 6 and 7, RLCA, RLCA, RLCA, and load the result into E, LD E, A.
ld a, h
and $07
or e
ld e, a
We load the third and the scanline in A, LD A, H, we keep the scanline, AND $07, we add the third that we have already placed correctly in E, OR E, and we load the result in E, LD E, A.
ld a, l
and $e0
rrca
rrca
or e
ret
We load the row and column in A, LD A, L, keep the row, AND $E0, put the value in bits 3 to 5, RRCA, RRCA, and add the third and scanline, OR E.
The final aspect of the routine is:
;--------------------------------------------------------------------
; Gets third, line and scanline of a memory location.
; Input: HL -> Memory location.
; Output: A -> Third, line and scanline obtained.
; Alters the value of the AF and E registers.
;--------------------------------------------------------------------
GetPtrY:
ld a, h ; A = H (third and scanline)
and $18 ; A = third
rlca
rlca
rlca ; Passes value of third to bits 6 and 7
ld e, a ; E = A
ld a, h ; A = H (third and scanline)
and $07 ; A = scanline
or e ; A OR E = Tercio and scanline
ld e, a ; E = A = TT000SSS
ld a, l ; A = L (row and column)
and $e0 ; A = line
rrca
rrca ; Passes line value to bits 3 to 5
or e ; A OR E = TTLLLLSSS
ret
This type of conversion was previously done in the checkVerticalLimit routine, but as it is needed in more than one routine, we have implemented it as a separate routine.
To test it, let’s modify the checkVerticalLimit routine by replacing almost all of it with a call to GetPtrY, as follows:
;--------------------------------------------------------------------
; Evaluates whether the vertical limit has been reached.
; Input: A -> Vertical limit (TTLLLLSSS).
; HL -> Current position (010TTSSS LLLCCCCCCC).
; Alters the value of the AF and BC registers.
;--------------------------------------------------------------------
checkVerticalLimit:
ld b, a ; B = A
call GetPtrY ; Y-coordinate (TTLLLSSSS)
; of the current position
cp b ; A = B? B = value A = vertical limit
ret
We compile, load in the emulator and check that nothing is broken.
We now implement the collision detection in the game.asm file.
We start with the routine that checks if there is a collision on the X axis. This routine gets the column in C where the collision occurs and activates the Z flag if it has occurred:
CheckCrossX:
ld a, (ballPos)
and $1f
cp c
ret
We load the position of the ball into A, LD A, (ballPos), leave the column, AND $1F, and compare the resulting value with the collision column, CP C.
The next step is to implement the routine that evaluates if there is a collision on the Y axis; it receives the position of the paddle in HL and triggers the Z flag if there is a collision:
CheckCrossY:
call GetPtrY
inc a
ld c, a
We get the Y coordinate of the paddle, CALL GetPtrY. As the first scanline of the paddle is white, we don’t take it into account for collisions, so we move on to the next one, INC A, and load the value into C, LD C, A.
ld hl, (ballPos)
call GetPtrY
ld b, a
We load the position of the ball into HL, LD HL, (ballPos), then get the Y coordinate, CALL GetPtrY, and load the value into B, LD B, A.
add a, $04
sub c
ret c
In A we have the Y-coordinate of the ball, we make it point to the penultimate scanline of the ball, which is the last one that is not a target, ADD A, $04, we subtract the Y-coordinate of the paddle, SUB C, and if there is a carry we exit, RET C, since the ball passes over it.
If we are not out, we must check if the ball goes under the paddle:
ld a, c
add a, $16
ld c, a
ld a, b
inc a
sub c
ret nc
xor a
ret
We load the Y-coordinate of the paddle into A, LD A, C, add $16 (22) to position ourselves on the penultimate scanline, the last one not equal to zero, ADD A, $16, and load the value into C, LD C, A.
We load the Y-coordinate of the ball into A, LD A, B, point to scanline 1, the first non-zero one, INC A, and subtract the Y-coordinate of the paddle, SUB C.
If there is no carry after the subtraction, we exit, RET NC, because either the ball passes underneath or it collides with the last scanline of the paddle, which is in the same Y-coordinate as the first scanline of the ball, and during the subtraction the Z-flag is activated.
If there is a carry, the ball collides with the rest of the paddle, so we activate the Z flag, XOR A, and exit, RET.
The next step is to implement the main routine, which we will call to check for collision, and in this case perform the necessary actions:
CheckBallCross:
ld a, (ballSetting)
and $40
jr nz, checkBallCross_left
We load the ball configuration into A, LD A, (ballSetting), and hold bit 6, AND $40, which indicates whether the ball goes right or left. If bit 6 is set to one, the ball goes left and we jump to check for a collision with player one’s paddle, JR NZ, checkBallCross_left.
If it does not jump, the ball goes right and we check for a collision with player two’s paddle:
checkBallCross_right:
ld c, CROSS_RIGHT
call CheckCrossX
ret nz
ld hl, (paddle2pos)
call CheckCrossY
ret nz
We load the collision column in C, LD C, CROSS_RIGHT, check if there is a collision on the X axis, CALL CheckCrossX. If there is no collision, we exit the routine, RET NZ.
If there is a collision on the X axis, we load the position of paddle two in HL, LD HL, (paddle2pos), and check if there is a collision on the Y axis, CALL CheckCrossY. If there is no collision, we exit the routine, RET NZ.
If we have not exited the routine, there has been a collision:
ld a, (ballSetting)
or $40
ld (ballSetting), a
ld a, $ff
ld (ballRotation), a
ret
We load the ball setting into A, LD A, (ballSetting), set bit 6 to one by shifting the ball address to the left, OR $40, and load the value into memory, LD (ballSetting), A.
We load minus one into A, LD A, $FF, change the ball rotation, LD (ballRotation), A, and exit the routine, RET.
Checking if there is a collision with player one’s paddle is similar to what we have seen before, we will copy the code and mark the differences without going into detail:
checkBallCross_left: ; Change!
ld c, CROSS_LEFT ; Change!
call CheckCrossX
ret nz
ld hl, (paddle1pos) ; Change!
call CheckCrossY
ret nz
ld a, (ballSetting)
and $bf ; Change!
ld (ballSetting), a
ld a, $01 ; Change!
ld (ballRotation), a
ret
The final aspect of the routines that check for collisions between the paddles and the ball is as follows:
;--------------------------------------------------------------------
; Assesses whether there is a collision between the ball
; and the paddles.
; Alters the value of the AF, C and HL registers.
;--------------------------------------------------------------------
CheckBallCross:
ld a, (ballSetting) ; A = ball direction/speed
and $40 ; A = bit 6 (left/right)
jr nz, checkBallCross_left ; Bit 6 = 1 goes left, skip
checkBallCross_right:
ld c, CROSS_RIGHT ; C = collision column
call CheckCrossX ; Collide X-axis?
ret nz ; No collision, exits
ld hl, (paddle2pos) ; HL = paddle position 2
call CheckCrossY ; Y-axis collision?
ret nz ; No collision, exits
; If it gets here there is a collision
ld a, (ballSetting) ; A = ball direction/speed
or $40 ; Change direction, left
ld (ballSetting), a ; Load to memory
ld a, $ff ; Change ball rotation
ld (ballRotation), a ; Load to memory
ret ; Sale
checkBallCross_left:
; Ball goes to the left
ld c, CROSS_LEFT ; C = collision column
call CheckCrossX ; Collide X-axis?
ret nz ; No collision, exits
ld hl, (paddle1pos) ; HL = paddle position 1
call CheckCrossY ; Y-axis collision?
ret nz ; No collision, exits
; If it gets here there is a collision
ld a, (ballSetting) ; A = ball direction/speed
and $bf ; Change direction, right
ld (ballSetting), a ; Load to memory
ld a, $01 ; Change ball rotation
ld (ballRotation), a ; Load to memory
ret ; Exits
;--------------------------------------------------------------------
; Evaluates whether the ball collides on the X-axis with the paddle.
; Input: C -> Column where the collision occurs.
; Exit: Z -> Collide.
; NZ -> No collision.
; Alters the value of the AF registers.
;--------------------------------------------------------------------
CheckCrossX:
ld a, (ballPos) ; A = line and column ball
and $1f ; A = column
cp c ; Is there a collision?
ret ; Exits
;--------------------------------------------------------------------
; Evaluates whether the ball collides in the Y-axis with the paddle.
; 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 (TTLLLSSS)
; Position returned points to the first scanline of the paddle that is 0
inc a ; A = second scanline paddle
ld c, a ; C = A
ld hl, (ballPos) ; HL = ball position
call GetPtrY ; Vertical ball position (TTLLLLSSS)
ld b, a ; B = vertical ball position
; Check if the ball goes over the paddle.
; The ball is composed of 1 scanline at 0, 4 at $3c and another at 0.
; The position points to the 1st scanline, and checks the collision
; with 5º.
add a, $04 ; A = ball position to 5th scanline
sub c ; A = A - C (paddle position)
ret c ; If carry, ball goes over the top
; Check if the ball passes under the paddle.
ld a, c ; A = vertical position paddle
add a, $16 ; A = penultimate scanline paddle
ld c, a ; C = A
ld a, b ; A = vertical position ball
inc a ; A = scanline 1 ball
sub c ; A = A - C (paddle position)
ret nc ; If no carry, ball passes underneath
; or the last scanline collides, in which
; case the Z flag is activated with SUB C
; in this case the Z flag is activated
; with SUB C
; There is a collision
xor a ; Active flag Z
ret
Now we just need to see if what we have implemented does what we want it to do.
Open main.asm and add the following line just below the loop_continue tag:
call CheckBallCross
We compile and load the emulator to see the results. If everything goes well, the ball hits the paddles, the collisions work.
The final appearance of the main.asm file is as follows:
; Collision detection
org $8000
;--------------------------------------------------------------------
; Programme entry
;--------------------------------------------------------------------
Main:
ld a, $00 ; A = 0
out ($fe), a ; Black border
call Cls ; Clear screen
call PrintLine ; Print 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 CheckBallCross ; Checks for collision between ball
; and paddles
call PrintBall ; Paint ball
call ReprintLine ; Reprint line
ld hl, (paddle1pos) ; HL = paddle 1 position
call PrintPaddle ; Paint paddle 1
ld hl, (paddle2pos) ; HL = paddle 2 position
call PrintPaddle ; Paint paddle 2
jr Loop ; Infinite loop
include "game.asm"
include "controls.asm"
include "sprite.asm"
include "video.asm"
countLoopBall: db $00 ; Count turns ball
countLoopPaddle: db $00 ; Count turns paddles
end $8000
ZX Spectrum Assembly, Pong
In the next ZX Spectrum Assembly chapter, we will implement the two-player game and the ball speed change.
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.