Being able to move around is great, but there’s still one object we need for this game: a ball! Just like with the paddle, the first step is to create a tile for the ball and load it into VRAM.
Add this to the bottom of your file along with the other graphics:
Ball: dw `00033000 dw `00322300 dw `03222230 dw `03222230 dw `00322300 dw `00033000 dw `00000000 dw `00000000 BallEnd:
Now copy it to VRAM somewhere in your initialization code, e.g. after copying the paddle’s tile.
; Copy the ball tile ld de, Ball ld hl, $8010 ld bc, BallEnd - Ball call Memcopy
In addition, we need to initialize an entry in OAM, following the code that initializes the paddle.
; Initialize the paddle sprite in OAM ld hl, _OAMRAM ld a, 128 + 16 ld [hli], a ld a, 16 + 8 ld [hli], a ld a, 0 ld [hli], a ld [hli], a ; Now initialize the ball sprite ld a, 100 + 16 ld [hli], a ld a, 32 + 8 ld [hli], a ld a, 1 ld [hli], a ld a, 0 ld [hli], a
As the ball bounces around the screen its momentum will change, sending it in different directions.
Let’s create two new variables to track the ball’s momentum in each axis:
SECTION "Counter", WRAM0 wFrameCounter: db SECTION "Input Variables", WRAM0 wCurKeys: db wNewKeys: db SECTION "Ball Data", WRAM0 wBallMomentumX: db wBallMomentumY: db
We will need to initialize these before entering the game loop, so let’s do so right after we write the ball to OAM. By setting the X momentum to 1, and the Y momentum to -1, the ball will start out by going up and to the right.
; Now initialize the ball sprite ld a, 100 + 16 ld [hli], a ld a, 32 + 8 ld [hli], a ld a, 1 ld [hli], a ld a, 0 ld [hli], a ; The ball starts out going up and to the right ld a, 1 ld [wBallMomentumX], a ld a, -1 ld [wBallMomentumY], a
Now for the fun part!
Add a bit of code at the beginning of your main loop that adds the momentum to the OAM positions.
Notice that since this is the second OAM entry, we use
+ 4 for Y and
+ 5 for X.
This can get pretty confusing, but luckily we only have two objects to keep track of.
In the future, we’ll go over a much easier way to use OAM.
Main: ld a, [rLY] cp 144 jp nc, Main WaitVBlank2: ld a, [rLY] cp 144 jp c, WaitVBlank2 ; Add the ball's momentum to its position in OAM. ld a, [wBallMomentumX] ld b, a ld a, [_OAMRAM + 5] add a, b ld [_OAMRAM + 5], a ld a, [wBallMomentumY] ld b, a ld a, [_OAMRAM + 4] add a, b ld [_OAMRAM + 4], a
You might want to compile your game again to see what this does. If you do, you should see the ball moving around, but it will just go through the walls and then fly offscreen.
To fix this, we need to add collision detection so that the ball can bounce around. We’ll need to repeat the collision check a few times, so we’re going to make use of two functions to do this.
Please do not get stuck on the details of this next function, as it uses some techniques and instructions we haven’t discussed yet. The basic idea is that it converts the position of the sprite to a location on the tilemap. This way, we can check which tile our ball is touching so that we know when to bounce!
; Convert a pixel position to a tilemap address ; hl = $9800 + X + Y * 32 ; @param b: X ; @param c: Y ; @return hl: tile address GetTileByPixel: ; First, we need to divide by 8 to convert a pixel position to a tile position. ; After this we want to multiply the Y position by 32. ; These operations effectively cancel out so we only need to mask the Y value. ld a, c and a, %11111000 ld l, a ld h, 0 ; Now we have the position * 8 in hl add hl, hl ; position * 16 add hl, hl ; position * 32 ; Convert the X position to an offset. ld a, b srl a ; a / 2 srl a ; a / 4 srl a ; a / 8 ; Add the two offsets together. add a, l ld l, a adc a, h sub a, l ld h, a ; Add the offset to the tilemap's base address, and we are done! ld bc, $9800 add hl, bc ret
The next function is called
IsWallTile, and it’s going to contain a list of tiles which the ball can bounce off of.
; @param a: tile ID ; @return z: set if a is a wall. IsWallTile: cp a, $00 ret z cp a, $01 ret z cp a, $02 ret z cp a, $04 ret z cp a, $05 ret z cp a, $06 ret z cp a, $07 ret
This function might look a bit strange at first.
Instead of returning its result in a register, like
a, it returns it in a flag:
If at any point a tile matches, the function has found a wall and exits with
If the target tile ID (in
a) matches one of the wall tile IDs, the corresponding
cp will leave
Z set; if so, we return immediately (via
ret z), with
But if we reach the last comparison and it still doesn’t set
Z, then we will know that we haven’t hit a wall and don’t need to bounce.
Putting it together
Time to use these new functions to add collision detection! Add the following after the code that updates the ball’s position:
BounceOnTop: ; Remember to offset the OAM position! ; (8, 16) in OAM coordinates is (0, 0) on the screen. ld a, [_OAMRAM + 4] sub a, 16 + 1 ld c, a ld a, [_OAMRAM + 5] sub a, 8 ld b, a call GetTileByPixel ; Returns tile address in hl ld a, [hl] call IsWallTile jp nz, BounceOnRight ld a, 1 ld [wBallMomentumY], a
You’ll see that when we load the sprite’s positions, we subtract from them before calling
You might remember from the last chapter that OAM positions are slightly offset; that is, (0, 0) in OAM is actually completely offscreen.
sub instructions undo this offset.
However, there’s a bit more to this: you might have noticed that we subtracted an extra pixel from the Y position. That’s because (as the label suggests), this code is checking for a tile above the ball. We actually need to check all four sides of the ball so we know how to change the momentum according to which side collided, so… let’s add the rest!
BounceOnRight: ld a, [_OAMRAM + 4] sub a, 16 ld c, a ld a, [_OAMRAM + 5] sub a, 8 - 1 ld b, a call GetTileByPixel ld a, [hl] call IsWallTile jp nz, BounceOnLeft ld a, -1 ld [wBallMomentumX], a BounceOnLeft: ld a, [_OAMRAM + 4] sub a, 16 ld c, a ld a, [_OAMRAM + 5] sub a, 8 + 1 ld b, a call GetTileByPixel ld a, [hl] call IsWallTile jp nz, BounceOnBottom ld a, 1 ld [wBallMomentumX], a BounceOnBottom: ld a, [_OAMRAM + 4] sub a, 16 - 1 ld c, a ld a, [_OAMRAM + 5] sub a, 8 ld b, a call GetTileByPixel ld a, [hl] call IsWallTile jp nz, BounceDone ld a, -1 ld [wBallMomentumY], a BounceDone:
That was a lot, but now the ball bounces around your screen! There’s just one last thing to do before this chapter is over, and thats ball-to-paddle collision.
Unlike with the tilemap, there’s no position conversions to do here, just straight comparisons.
However, for these, we will need the carry flag.
The carry flag is notated as
C, like how the zero flag is notated as
Z, but don’t confuse it with the
A refresher on comparisons
Z, you can use the carry flag to jump conditionally.
Z is used to check if two numbers are equal,
C can be used to check if a number is greater than or smaller than another one.
cp a, b sets
a < b, and clears it if
a >= b.
(If you want to check
a <= b or
a > b, you can use
C in tandem with two
Armed with this knowledge, let’s work through the paddle bounce code:
; First, check if the ball is low enough to bounce off the paddle. ld a, [_OAMRAM] ld b, a ld a, [_OAMRAM + 4] cp a, b jp nz, PaddleBounceDone ; If the ball isn't at the same Y position as the paddle, it can't bounce. ; Now let's compare the X positions of the objects to see if they're touching. ld a, [_OAMRAM + 5] ; Ball's X position. ld b, a ld a, [_OAMRAM + 1] ; Paddle's X position. sub a, 8 cp a, b jp c, PaddleBounceDone add a, 8 + 16 ; 8 to undo, 16 as the width. cp a, b jp nc, PaddleBounceDone ld a, -1 ld [wBallMomentumY], a PaddleBounceDone:
The Y position’s check is simple, since our paddle is flat. However, the X position has two checks which widen the area the ball can bounce on. First we add 16 to the ball’s position; if the ball is more than 16 pixels to the right of the paddle, it shouldn’t bounce. Then we undo this by subtracting 16, and while we’re at it, subtract another 8 pixels; if the ball is more than 8 pixels to the left of the paddle, it shouldn’t bounce.
You might be wondering why we checked 16 pixels to the right but only 8 pixels to the left. Remember that OAM positions represent the upper-left corner of a sprite, so the center of our paddle is actually 4 pixels to the right of the position in OAM. When you consider this, we’re actually checking 12 pixels out on either side from the center of the paddle.
12 pixels might seem like a lot, but it gives some tolerance to the player in case their positioning is off. If you’d prefer to make this easier or more difficult, feel free to adjust the values!
BONUS: tweaking the bounce height
You might notice that the ball seems to “sink” into the paddle a bit before bouncing. This is because the ball bounces when its top row of pixels aligns with the paddle’s top row (see the image above). If you want, try to adjust this so that the ball bounces when its bottom row of pixels touches the paddle’s top.
Hint: you can do this with just a single instruction!
ld a, [_OAMRAM] ld b, a ld a, [_OAMRAM + 4] + sub a, 6 cp a, b
Alternatively, you can add
add a, 6 just after
ld a, [_OAMRAM].
In both cases, try playing with that
6 value; see what feels right!