Text
Fonts
Today we're going to get very technical and talk about custom fonts in PICO-8. I know I just made this blog, but we're taking a deep dive right away because it's one of the first things I did in my game, and it's still fresh in my mind.
PICO-8 ships with a default character set called P8SCII (a pun on ASCII) that includes 256 different characters. This set includes uppercase letters, lowercase letters, numbers, punctuation, hiragana, katakana, various control codes, and other special glyphs. You can see it below.
The default font has some pros and cons. If we focus on the uppercase Latin letters, punctuation, and numbers specifically, all of the characters conform to a 3x5 pixel size. This is very space efficient and consistent, which makes it an excellent font for writing code on PICO-8's tiny 128x128 screen.
Code is written in "all caps" so to speak, although the concept of uppercase and lowercase letters doesn't really exist in PICO-8 programming. Instead we have normal letters (seen above) as well as an alternate set of 26 characters called "puny characters," which looks similar to the regular font but even smaller!
So it works great for coding, but what if we're reading a lot of English words? Here's an excerpt from Homer's Iliad written using the default font, with regular characters for uppercase and puny characters for lowercase.
It doesn't look bad necessarily. I'd even say it conveys a certain amount of retro charm. But I'd be lying if I said it was easy to read.
Enter custom fonts! This is a relatively new feature of PICO-8, and it is one that is a little bit complicated to implement and not very well-documented at the moment, but I'm going to do my best to explain it.
Unfortunately, you can't simply have your custom font loaded and ready to go by setting it up in the cart itself. Custom fonts are defined by messing around with specific memory values at runtime. I'll explain an easy way to actually create and load your own font in another post, but for now lets talk about the memory addresses that need to be modified and how it works under the hood.
First of all, custom fonts must be enabled with the following poke:
poke(0x5f58,0x81)
Address 0x5f58 contains the bitfield for the current character rendering mode. There are a few options, but the ones we care about are 0x01, which enables special rendering modes entirely, and 0x80, which enables the custom font.
Once enabled, all future printing operations will use a custom font that is defined in RAM from addresses 0x5600 to 0x5dff. That's 2,048 bytes total. Remember that there are 256 different characters in P8SCII, so that's 2,048 / 256 = 8 bytes per character.
Each character in the custom font can be as large as 8x8 pixels in size. Each of the 8 reserved bytes for a character represents a row of pixels, and within each byte, each bit represents whether the cell is filled or not, with the least significant bit being the leftmost column.
Here's an example. Capital T has index 116 in P8SCII. 116 x 8 = 928, which is 0x03a0 in hexadecimal, so the custom font for T is defined at 0x5600 + 0x03a0 = 0x59a0. Here is how I have defined T in my custom font.
peek(0x59a0) = 0b00011111 = '#####---' peek(0x59a1) = 0b00000100 = '--#-----' peek(0x59a2) = 0b00000100 = '--#-----' peek(0x59a3) = 0b00000100 = '--#-----' peek(0x59a4) = 0b00000100 = '--#-----' peek(0x59a5) = 0b00000100 = '--#-----' peek(0x59a6) = 0b00000000 = '--------' peek(0x59a7) = 0b00000000 = '--------'
(Remember, the least significant bit is the leftmost pixel drawn, thus the binary representation and the actual drawing appear to be mirrored.)
It doesn't look great on Tumblr, but hopefully you get the gist. As you can see, this character is 5x6 pixels in size. But how will PICO-8 draw this? The two highest addresses (0x59a6 and 0x59a7) are still empty, and there are also still 3 columns of space on the right. Obviously we don't want the T to be floating in the air or have a big gap before the next character is printed, but how would PICO-8 know that?
Here is where things get more complicated. There are two components that define the actual size of a character.
The default size of the entire custom font. This defines a height and width which applies to ALL characters in the custom font.
An individualized width and height offset for all characters. This is a newer feature than the above, and it is what allows us to make variable-width fonts! Because it's newer, you'll see a lot of resources on how to do #1, but resources for how to do #2 are relatively sparse, at least at the time of writing!
Where do we define this stuff? Remember when I said that we have 8 bytes for each of the 256 characters in RAM for defining the custom font? There's a slight asterisk there, which is that the first 16 characters in P8SCII are never actually printed. These are called control codes and they represent various printing functions, not text. They do have a graphic in the first image I posted, but they are never actually printed. Therefore, the bytes in RAM reserved for these first 16 characters may be freely appropriated for other uses.
Font-wide settings are stored in the data reserved for character 0. We can think of this as the "font header". Each of its 8 bytes have different purposes. I'll outline them explicitly.
0x5600: Pixel width of characters 16-127. (Mainly Latin letters, numbers, and punctuation.) 0x5601: Pixel width of characters 128-255. (Mainly special glyphs and Japanese letters.) 0x5602: Pixel height of all characters. 0x5603: Draw offset x. 0x5604: Draw offset y. 0x5605: Set 0x1 to apply size adjustments (see below); set 0x2 to apply tabs relative to the cursor. 0x5606: Pixel width of tabs. 0x5607: Unused.
I'm just going to focus on 0x5600 and 0x5602 here, but you can experiment with other values as you wish. When designing my font, I found that 4 pixels wide generally looked pretty good. However, we also have to account for a gap between characters, which I would like to just be 1 pixel, so our actual character width should be 5, which is the value I poked to 0x5600.
For height, Latin characters vary quite a bit. For most tall characters, like lowercase d, I made them 6 pixels tall. However, some characters like lowercase g are meant to be drawn partially "below the line" when written on paper, so I had these characters dip 2 pixels deeper. Here are some examples.
Since we have characters that extend to both extremes in terms of height, we can say that overall the font is meant to be 8 pixels tall. Furthermore, I once again want a 1 pixel vertical gap when printing to the next line, so we'll say the height is technically 9, which is the value I poked to 0x5602.
But wait! What about the characters which are extra wide, like our uppercase T? That was 5 pixels wide, not 4, so won't it get chopped off? The answer is yes. (Technically, the entire T would still get printed, but we would lose the 1 pixel gap before the next character.)
This is where the per-character width and height adjustments come into play. Remember the data for character 0 formed our header, but we still have characters 1-15 whose data is still unused. That's 15x8=120 bytes. We have 240 printable characters (17..255). Therefore we can allocate one nibble (that is, 4 bits, or half a byte) for each printable character. The lower nibble in each byte will correspond to the lower of the two characters represented by that byte.
The 3 lowest bits in the nibble represent the width offset for that character as a signed 3-bit integer, for a range of -4 to +3. This is what will be added to the default width when determining the actual width for that specific character. The highest bit is used for a vertical offset; if it is set, the character will be drawn one pixel higher than normal.
Finally, to enable these per-character adjustments at all, the lowest bit of 0x5605 must be set to 1, as mentioned previously.
Let's see some examples. Remember that the T we drew earlier was 5x6. Or rather, it's 6x6 if we include the 1 pixel horizontal padding on the right side. But our default width is only 5. Therefore we need to set T's offset to +1.
We have to do some fairly complicated address math to figure out where T's offset lives. Remember its P8SCII index is 116. Among the actually printable characters, its index is 116-16=100. Offset data begins at 0x5608. Remember each byte corresponds to two characters. 100 (or 0x64) is an even number, so it is going to live in the lower nibble of its corresponding byte, which is located at 0x5608 + 0x64 / 2 = 0x563a.
T's roommate is character 117, which naturally, is U. The U I drew conforms to the standard width of 5, so its offset should just be 0. Across the board, I'm not using any height offsets, so those bits will always be 0. Therefore, our offset for T is achieved with the following poke:
poke(0x563a, 0x01)
Lets pick a more complicated example: lowercase L and M. Let me show you what I drew for those first.
Both of these characters have nonstandard widths. Worse yet, they're roommates! L will need an offset of -2, and m will need +1.
L's index is 76, and M's is 77. Their offset data will live in the same byte, with L taking the lower nibble. The address of this data is given by:
0x5608 + (76 - 16) / 2 = 0x562e.
M needs a width offset of 1, which is simply represented as 001 in binary. L needs -2, which is represented as 110 using two's complement on a 3 bit integer. Again, both should have a 0 height offset. Putting this all together, our poke will be:
poke(0x5626, 0b00010110)
Or in hex:
poke(0x5626, 0x16)
Or even in decimal:
poke(0x5626, 22)
Anyway, that's my overlong explanation for how custom fonts work. Now let's see the custom font I made in action, side-by-side with the default font.
Not bad! Personally, I find this dramatically more readable, but it does come at a cost: it takes up way more vertical screen space than before.
5 notes
·
View notes
Text
Hello pico-8!
Welcome to my blob! I mean blog!
Lately I've been messing around with pico-8, a really fun game dev environment created by Lexaloffle Games. I'll be documenting my progress here.
So what's special about pico-8? It's a "fantasy computer" meant to resemble old home computers from the 80's like the Commodore 64. The hardware capabilities of the pico-8 are extremely limited, but these restrictions only exist to breed creativity. Pico-8 also comes with built-in editors for code, sprites, music, and more, so it's actually really easy to make games for it, in spite of the technical limitations.
Here's a basic overview of the pico-8's specs and limitations.
128x128 pixel resolution. Even smaller than a GameBoy screen!
16-color palette, and the colors are all pre-defined. However, there are also 16 "secret" colors you can use... Kind of. I'll make a post about that some other time.
Games are written in the Lua programming language. One cart is limited to a maximum of 8,192 tokens and 65,535 total characters!
4 sound channels
128 8x8 sprites.
One 128x32 tile map.
Enough shared space for either another sprite sheet, another map, or some combination of the two.
Up to 8 controllers, each one having 6 digital buttons and no analog.
See for yourself what kind of cool games people have published for this fun device!
2 notes
·
View notes