The background is very useful when the whole screen should move at once, but this is not ideal for everything. For example, a cursor in a menu, NPCs and the player in a RPG, bullets in a shmup, or balls in an Arkanoid clone… all need to move independently of the background. Thankfully, the Game Boy has a feature that’s perfect for these! In this lesson, we will talk about objects (sometimes called “OBJ”).
The above description may have made you think of the term “sprite” instead of “object”. The term “sprite” has a lot of meanings depending on context, so, to avoid confusion, this tutorial tries to use specific alternatives instead, such as object, metasprite, actor, etc.
Each object allows drawing one or two tiles (so 8×8 or 8×16 pixels, respectively) at any on-screen position—unlike the background, where all the tiles are drawn in a grid. Therefore, an object consists of its on-screen position, a tile ID (like with the tilemap), and some extra properties called “attributes”. These extra properties allow, for example, to display the tile flipped. We’ll see more about them later.
Just like how the tilemap is stored in VRAM, objects live in a region of memory called OAM, meaning Object Attribute Memory. Recall from above that an object consists of:
- Its on-screen position
- A tile ID
- The “attributes”
These are stored in 4 bytes: one for the Y coordinate, one for the X coordinate, one for the tile ID, and one for the attributes. OAM is 160 bytes long, and since 160 ∕ 4 = 40, the Game Boy stores a total of 40 objects at any given time.
There is a catch, though: an object’s Y and X coordinate bytes in OAM do not store its on-screen position! Instead, the on-screen X position is the stored X position minus 8, and the on-screen Y position is the stored Y position minus 16. To stop displaying an object, we can simply put it off-screen, e.g. by setting its Y position to 0.
These offsets are not arbitrary! Consider an object’s maximum size: 8 by 16 pixels. These offsets allow objects to be clipped by the left and top edges of the screen. The NES, for example, lacks such offsets, so you will notice that objects always disappear after hitting the left or top edge of the screen.
Let’s discover objects by experimenting with them!
First off, when the Game Boy is powered on, OAM is filled with a bunch of semi-random values, which may cover the screen with some random garbage.
Let’s fix that by first clearing OAM before enabling objects for the first time.
Let’s add the following just after the
ld a, 0 ld b, 160 ld hl, _OAMRAM ClearOam: ld [hli], a dec b jp nz, ClearOam
This is a good time to do that, since just like VRAM, the screen must be off to safely access OAM.
Once OAM is clear, we can draw an object by writing its properties.
ld hl, _OAMRAM ld a, 128 + 16 ld [hli], a ld a, 16 + 8 ld [hli], a ld a, 0 ld [hli], a ld [hl], a
Remember that each object in OAM is 4 bytes, in the order Y, X, Tile ID, Attributes. So, the object’s top-left pixel lies 128 pixels from the top of the screen, and 16 from its left. The tile ID and attributes are both set to 0.
You may remember from the previous lesson that we’re already using tile ID 0, as it’s the start of our background’s graphics. However, by default objects and backgrounds use a different set of tiles, at least for the first 128 IDs. Tiles with IDs 128–255 are shared by both, which is useful if you have a tile that’s used both on the background and by an object.
If you go to “Tools”, then “Tile Viewer” in Emulicious’ debugger, you should see three distinct sections.
Because we need to load this to a different area, we’ll use the address $8000 and load a graphic for our game’s paddle.
Let’s do so right after
; Copy the tile data ld de, Paddle ld hl, $8000 ld bc, PaddleEnd - Paddle CopyPaddle: ld a, [de] ld [hli], a inc de dec bc ld a, b or a, c jp nz, CopyPaddle
And don’t forget to add
Paddle to the bottom of your code.
Paddle: dw `13333331 dw `30000003 dw `13333331 dw `00000000 dw `00000000 dw `00000000 dw `00000000 dw `00000000 PaddleEnd:
Finally, let’s enable objects and see the result.
Objects must be enabled by the familiar
rLCDC register, otherwise they just don’t show up.
(This is why we didn’t have to clear OAM in the previous lessons.)
We will also need to initialize one of the object palettes,
There are actually two object palettes, but we’re only going to use one.
; Turn the LCD on ld a, LCDCF_ON | LCDCF_BGON | LCDCF_OBJON ld [rLCDC], a ; During the first (blank) frame, initialize display registers ld a, %11100100 ld [rBGP], a ld a, %11100100 ld [rOBP0], a
Now that you have an object on the screen, let’s move it around.
Done loop did nothing; let’s rename it to
Main and use it to move our object.
We’re going to wait for VBlank before changing OAM, just like we did before turning off the screen.
Main: ; Wait until it's *not* VBlank ld a, [rLY] cp 144 jp nc, Main WaitVBlank2: ld a, [rLY] cp 144 jp c, WaitVBlank2 ; Move the paddle one pixel to the right. ld a, [_OAMRAM + 1] inc a ld [_OAMRAM + 1], a jp Main
Here, we are accessing OAM without turning the LCD off, but it’s still safe. Explaining why requires a more thorough explanation of the Game Boy’s rendering, so let’s ignore it for now.
Now you should see the paddle moving… very quickly. Because it moves by a pixel every frame, it’s going at a speed of 60 pixels per second! To slow this down, we’ll use a variable.
So far, we have only worked with the CPU registers, but you can create global variables too!
To do this, let’s create another section, but putting it in
WRAM0 instead of
Unlike ROM (“Read-Only Memory”), RAM (“Random-Access Memory”) can be written to; thus, WRAM, or Work RAM, is where we can store our game’s variables.
Add this to the bottom of your file:
SECTION "Counter", WRAM0 wFrameCounter: db
Now we’ll use the
wFrameCounter variable to count how many frames have passed since we last moved the paddle.
Every 15th frame, we’ll move the paddle by one pixel, slowing it down to 4 pixels per second.
Don’t forget that RAM is filled with garbage values when the Game Boy starts, so we need to initialize our variables before first using them.
ld a, 0 ld [wFrameCounter], a Main: ld a, [rLY] cp 144 jp nc, Main WaitVBlank2: ld a, [rLY] cp 144 jp c, WaitVBlank2 ld a, [wFrameCounter] inc a ld [wFrameCounter], a cp a, 15 ; Every 15 frames (a quarter of a second), run the following code jp nz, Main ; Reset the frame counter back to 0 ld a, 0 ld [wFrameCounter], a ; Move the paddle one pixel to the right. ld a, [_OAMRAM + 1] inc a ld [_OAMRAM + 1], a jp Main
Alright! Up next is us taking control of that little paddle.