Sprite Compression in Pico-8


When I started Alpine Climb, I made the decision to use relatively large sprites for the player, knowing that I would likely run out of room on the Pico-8 spritesheet. Pico-8 allows you to use a 128x128 space to use for sprites, and at 16x32 my character's animations would use that up pretty quickly! However, there is a reason for my choice: it would force me to learn compression techniques to squeeze more sprites into my game than would normally be possible.

Pico-8 does not have any built-in compression for images, but there is a compression library published by the creator of Pico-8. It's called PX9 and the decompression code is very short - essential for use in Pico-8! So naturally I decided to use that. It's not quite a simple as just compressing the spritesheet, though: Pico-8 still has the limitation of drawing from a 128x128 spritesheet for its built-in-functions. So if you want to use more than that, which is likely is you're using compression in the first place, you need to find creative ways to work around that.

I still wanted to use Pico-8's builtin functions as much as possible, so I decided I'd be decompressing my images to the spritesheet at runtime, allowing Pico-8 to draw them as normal. Here's the basic approach:

  1. Create a new cart with the uncompressed sprites - I called this `alpine_sprites`
  2. Copy the PX9 cart (linked above) and edit the `_init` function to do the compression - I called this `comp`
  3. Compress the sprites to another new cart called `alpine_c` (this is optional, but avoids accidentally overwriting the real cart data)
  4.  Manually copy the compressed sprites to the actual cart, `alpine`.
  5. Copy the `px9_decomp` function into `alpine` and use it to decompress the sprites in the game's `_init`

The first compression code I used (taken from the BBS page) was:

reload(0x0, 0x0, 0x2000, "alpine_sprites.p8")
clen = px9_comp(0, 0, 128, 128, 0x2000, sget)
cstore(0x0, 0x2000, clen, "alpine_c.p8")

What this code does is load the `alpine_sprites` spritesheet into memory, compress the entire 128x128 range into the memory starting at 0x2000 (the map data), then store that compressed data into the spritesheet of `alpine_c`.

The compression varies, but using this technique I was able to compress about 3 pages worth of sprites into 1. The resulting spritesheet is just noise, but it looks pleasing!


I copied this to page 4 of my game's spritesheet and decompressed it as follows:

px9_decomp(0,0,0x1800,sget,sset)
px9_decomp(64,0,0x18b6,sget,sset)

The 0x1800 here is the address for the start of page 4 of the spritesheet, while 0x18b6 is calculated from the start address + length of previous data. You can get the length when doing the compression - clen in the code above - and converting it to hex.

This worked, but I soon realised it wasn't going to help as much as I thought it would. The compressed data takes up one page of the spritesheet, and the decompressed data takes about two (one for the backdrop and one for the player sprites, depending on game stage). That only left me with one more page to draw on, which is the same as I had before! At first I thought I tried to decompress the sprites over the top of the compressed sprite page, but that wouldn't work when I needed to decompress multiple sprites because the compressed data would be overwritten. Fortunately though, there is a way to make it work...

Pico-8 reserves some area of memory for user data, including the range 0x8000-0xffff. Aha! This means that I don't need to keep the compressed data taking up space in the spritesheet at runtime; when the program loads, I can simply move it to to free user data, leaving the 4th page free for decompressing sprites. In my _init function I added this code to copy the 4th page of sprites (0x1800) to user data:

memcpy(0x8000,0x1800,2048)

Then, I updated the decompression code to use this new area of memory:

px9_decomp(0,96,0x8000,sget,sset)
px9_decomp(64,88,0x80b6,sget,sset)

This worked as expected, and I could then load my sprites from page 4 at runtime without affecting the compressed data. Great! This meant I had freed up an extra page of sprites to use how I like. Currently, my use of the spritesheet looks like this:

  • Cartridge data:
    • Page 1-3 are empty
    • Page 4 is compressed sprites
  • Runtime data:
    • Page 1-2 are unused
    • Page 3-4 are for decompressed sprites

I expect to use the last 2 pages for compressed data, and these are also used as the buffer at runtime, meaning I can display a maximum of 2 pages of sprites at the same time on the screen. I need to add in code to decompress the sprites whenever my game transitions to a new stage, meaning that the sprites I want to use are swapped in and out of the spritesheet as needed. I now have more than enough room on the spritesheet to finish my game!

Get Alpine Climb

Download NowName your own price