In previous entries we finally got to a point in which our custom palettes are loaded and ready to be used. Now, it's time to find out which values to change to apply them to the graphics, and more specifically in this entry, to the sprites.
Metasprite data
Each Game Boy game stores metasprite data in a different way. In my experience, the most common way to store it is in a table that follows the same sequence as the OAM. That is, sequential 4-byte blocks that define the Y-position, X-position, tile number, and attributes of each sprite respectively.
A special byte/bit is commonly used to signify the end of the metasprite data, as the game needs a way to know where it ends. In some older games, structures with no attribute byte are a bit more common, as well as fixed size metasprites. In those cases, you would probably be forced to make major changes to the data structure and/or the metasprite printing routine to have complete freedom over the metasprites.
Kirby's Dream Land follows the aforementioned OAM sequence with a small caveat. Let's take a look at Kirby's metasprite (more on how to find this later):
Y position | X position | Tile Index | Attributes |
---|---|---|---|
F8 | F8 | 02 | 00 |
F8 | 00 | 12 | 01 |
This corresponds to the data in our OAM viewer:
If you have a keen eye, you'll notice the attributes byte is being used here, even though it's supposed to be CGB only. That's why in our current build Kirby only has half of his body colored. Of course, as anticipated before, this is because this game uses bit 0 as a flag to indicate the end of the metasprite.
While this is not uncommon, it's way more frequent to have a separate byte for this purpose, meaning that usually no major data or code changes are required.
Planning our approach
In short, we just learned that in order to have attributes freedom on any sprite, we have to find a way to signify the metasprite has ended in a different way.
Three potential approaches come to mind:
- Use bit 4 instead of bit 0 as the end flag
- Use bits 3-1 instead of bits 2-0 for the palettes
- Add an end byte to every metasprite
Approach #3 would require too much ROM and approach #2 would make palette assignment a bit less intuitive, so I'll stick with #1.
This presents a little problem, as we have to change not only the code but the data for all metasprites. Still, it's not a big deal, we'll just have to write a small script.
Finding our data
The general process of finding routines and data has been explained in a previous entry, but as a reminder, breakpoints are set in the debugger and the code is followed to higher levels until it is understood where it came from. A look at the complete disassembly of the ROM can also be useful.
In this case, we'll start by taking a look at the DMA register, which takes us to address $C000
. After setting a write breakpoint, we can find Kirby's data comes from B:$58B9
.
Next, we want the metasprite pointers so we can easily move around metasprites if needed. For this, we set a read breakpoint at Kirby's metasprite data (again, at B:58B9
) and continue to set execution breakpoints a bit back in the routine until we find where it starts. This should take us to $18FF
, which after a quick search in our disassembly we find it's executed from a jump at $2CE0
.
There, we find the pointers are stored in WRAM at $D1C0
, so one more write breakpoint is needed. We finally find the data we're looking for at $15F1
, with the pointers being prefixed with a byte probably related to its animation.
As an exercise, I strongly recommend you get your favorite debugger and try to follow the same path I did. It's a great way to get familiar with the process.
Editing the data
At this point, the best thing we can do (and actually what I would normally do) is to completely reverse the metasprites format, turn all pointers into labels and defining anything that's necessary to freely expand the metasprites without having to move data around. However, as we can already achieve what we want be deallocating the metasprites that need to be expanded, I'll consider that as beyond the scope of this tutorial.
Instead, we will store the metasprite data in a format that allows us to easily modify it. For this, we'll have a separate asm file with the following format:
.BANK $0B SLOT 1
.ORGA $58B9
.SECTION "Kirby metasprite data" OVERWRITE
Kirby:
@000:
.db $F8,$F8,$02,$00
.db $F8,$00,$12,$10
@001:
.db $F8,$F8,$04,$00
.db $F8,$00,$14,$10
@002:
.db $F8,$F8,$06,$00
.db $F8,$00,$16,$10
; ...
.ENDS
You might have noticed that the end flag is now in bit 4 of the attributes byte, which needs to be changed for every single metasprite. Assuming we have the relevant data in a binary file, we can use a script like the following to extract it in the format we need:
- JavaScript
- Python
- Rust
const fs = require('fs');
function extractData(inputFilename, outputFilename) {
const inputFile = fs.openSync(inputFilename, 'r');
const outputFile = fs.openSync(outputFilename, 'w');
let byteCount = 0;
let iterationCount = 0;
fs.writeSync(outputFile, `@${iterationCount.toString().padStart(3, '0')}:\n.db `);
iterationCount += 1;
const buffer = Buffer.alloc(1);
while (true) {
const bytesRead = fs.readSync(inputFile, buffer, 0, 1);
if (bytesRead === 0) {
break;
}
byteCount += 1;
const hexValue = buffer[0].toString(16).toUpperCase().padStart(2, '0');
let modifiedValue;
if (byteCount % 4 === 0 && (parseInt(hexValue, 16) & 0x01)) { // Checks bit 0
modifiedValue = (parseInt(hexValue, 16) & 0xFE | 0x10).toString(16).toUpperCase().padStart(2, '0'); // Sets bit 4 and unset bit 0
} else {
modifiedValue = hexValue;
}
fs.writeSync(outputFile, `$${modifiedValue}`);
if (modifiedValue !== hexValue) {
fs.writeSync(outputFile, `\n\n@${iterationCount.toString().padStart(3, '0')}:`);
iterationCount += 1;
}
if (byteCount % 4 === 0) {
fs.writeSync(outputFile, '\n.db ');
} else {
fs.writeSync(outputFile, ',');
}
}
fs.closeSync(inputFile);
fs.closeSync(outputFile);
}
const inputFilename = 'data.bin';
const outputFilename = 'data.txt';
extractData(inputFilename, outputFilename);
def extract_data(input_filename, output_filename):
with open(input_filename, 'rb') as input_file, open(output_filename, 'w') as output_file:
byte_count = 0
iteration_count = 0
output_file.write(f'@{iteration_count:03}:\n.db ')
iteration_count += 1
while True:
byte = input_file.read(1)
if not byte:
break
byte_count += 1
hex_value = format(byte[0], '02X')
if byte_count % 4 == 0 and int(hex_value, 16) & 0x01: # Checks bit 0
modified_value = format(int(hex_value, 16) & 0xFE | 0x10, '02X') # Sets bit 4 and unset bit 0
else:
modified_value = hex_value
output_file.write(f'${modified_value}')
if(modified_value != hex_value):
output_file.write(f'\n\n@{iteration_count:03}:')
iteration_count += 1
if byte_count % 4 == 0:
output_file.write('\n.db ')
else:
output_file.write(',')
if __name__ == "__main__":
input_filename = "data.bin"
output_filename = "data.txt"
extract_data(input_filename, output_filename)
use std::fs::File;
use std::io::{Read, Write};
fn extract_data(input_filename: &str, output_filename: &str) {
let mut input_file = File::open(input_filename).expect("Unable to open input file");
let mut output_file = File::create(output_filename).expect("Unable to create output file");
let mut byte_count = 0;
let mut iteration_count = 0;
write!(output_file, "@{:03}:\n.db ", iteration_count).expect("Error writing to output file");
iteration_count += 1;
let mut buffer = [0u8; 1];
while let Ok(bytes_read) = input_file.read(&mut buffer) {
if bytes_read == 0 {
break;
}
byte_count += 1;
let hex_value = format!("{:02X}", buffer[0]);
let modified_value = if byte_count % 4 == 0 && (u8::from_str_radix(&hex_value, 16).unwrap() & 0x01) != 0 { // Checks bit 0
format!("{:02X}", u8::from_str_radix(&hex_value, 16).unwrap() & 0xFE | 0x10) // Sets bit 4 and unset bit 0
} else {
hex_value.clone()
};
write!(output_file, "${}", modified_value).expect("Error writing to output file");
if modified_value != hex_value {
write!(output_file, "\n\n@{:03}:", iteration_count).expect("Error writing to output file");
iteration_count += 1;
}
if byte_count % 4 == 0 {
write!(output_file, "\n.db ").expect("Error writing to output file");
} else {
write!(output_file, ",").expect("Error writing to output file");
}
}
}
fn main() {
let input_filename = "data.bin";
let output_filename = "data.txt";
extract_data(input_filename, output_filename);
}
Of course, we also need to update our code so the end flag is checked correctly. If we set a read breakpoint at the attribute byte, we can very easily find the code that checks for the end flag:
; ...
inc hl
inc e
bit 0, a ; $192B: Checks if the metasprite has ended
jr z,@Loop
ld a,e
; ...
Not much to say here, all we need to do is to change it for bit 4, a
:
.BANK 0 SLOT 0
.ORG $192B
.SECTION "Change Metasprite Flag" OVERWRITE
bit 4,a
.ENDS
We can now compile our ROM and see the results:
While Kirby's sprites are now correctly colored, we can observe that the enemies and other metasprites are not displayed correctly. This is because they are not using the same metasprite data, so we'll have to find it and apply the same principles.
I'll leave that as an exercise for you, but if you need a hint, you can find all you need in the linked code down below.
And that's it! Believe it or not, we already have everything we need to color all the sprites in the game — it's just a matter of having the appropriate palette set loaded at the right time and assigning the correct palette to each sprite. While we could have done this dynamically by loading palettes as enemies spawn, for most games this is an unnecessary overkill.
After asigning a couple of palettes, our current build should look like this:
In following posts we will be dealing with the backgrounds so that we can start to have a more complete final look.
Check out the code so far in GitHub