PICO-8 Spritesheet Shenanigans
One of the most common problems that I encounter when working with PICO-8 is that I have more sprites than the spritesheet will allow me to fit. This has caused me a lot of headache in the past when attempting to chop sprites into smaller pieces and drawing each one properly in code. In the past few months I’ve found multiple ways to circumvent this (all at the cost of some performance) and thought I would outline my findings here.
Back in late October, Chris West and I wanted to make a Halloween update for PICO NIGHT PUNKIN’, which meant we needed to find a way to fit Skid and Pump into the spritesheet. Each character in PICO NIGHT PUNKIN’ has 6 sprites total, but Skid and Pump has 7, and has a much larger bounding box, meaning it would have to be split up in some way.
While this didn’t require much fancy code beyond positioning the different pieces of each sprite, it DID require some funky workarounds when it came to the main menu. We already had a full spritesheet, and while we had 2 more multicarts to spare, we didn’t want to split the menu up like that, so I did some digging and stumbled upon this blog post which outlined methods of swapping spritesheets at runtime by packing a spritesheet into a string
The cart above contains two spritesheets, one as the main PICO-8 spritesheet, and the other stored as a string in code. The blog post goes into good detail on how this method of spritesheet swapping works, and was good enough for us to implement in PICO NIGHT PUNKIN’.
The method I settled on was using the
#GFX_EXPORT cart to convert our secondary spritesheet to a string. I then used the
restore_gfx functions from the blog post to swap between the spritesheets as needed. However, switching spritesheets is very resource intensive, and causes the game to lag a bit when the spritesheet is swapped. As a result, whenever you hover over South in the menu, you’ll notice the game freeze for a brief moment, but it was a sacrifice we had to make.
Fast-forward a month or so. I’m making PICO TOADS and I realize that due to the size of the spritesheet, I’m pretty limited as to how many features I can include. There were ways for me to fit a majority of what I wanted, but not without headache. So I decided to go the route of multiple spritesheets!
While I totally could have fit the bases, eyes and mouths in one spritesheet, I instead developed a system that would allow for me to easily add and remove new features at will. I used the
#GFX_EXPORT cart again to convert the spritesheets to a strings and stored them in a table called
sheets. I then created a function that loads a spritesheet based on it’s index (with 0 being the cart’s main spritesheet since lua tables index from 1)
function load_sheet(i) if i == 0 then reload(0x0,0x0,0x2000) else gfx = sheets[i] index=0 for i=1,#gfx,2 do count=hex2num(sub(gfx,i,i)) col=hex2num(sub(gfx,i+1,i+1)) for j=1,count do sset((index)%128,flr((index)/128),col) index+=1 end end end end --requires this function (from the blog post mentioned prior) function hex2num(str) return ("0x"..str)+0 end
This allowed me to load each spritesheet sequentially and draw each feature without clearing the screen. Obviously there is very high faux-CPU usage from this happening all at once, but it looks very natural when the toad is finished!
Then, after releasing PICO TOADS, I had a breakthrough. I already knew that I wanted to make a follow-up to the project, but wasn’t sure what I would do exactly. But then I thought, “what if I re-constructed an entirely new spritesheet from all the others on initialization?”, that would allow me to draw all of the toad’s features without having to switch spritesheets. So I got to work at creating a function that would loop through each spritesheet (as a string) and load it into memory. After a spritesheet is loaded into memory, the chosen feature is saved to a table, and once all the tables have been cycled through, the final spritesheet is constructed by taking each table and drawing them to the spritesheet. The following cart uses a snippet of code from ANIMATED PICO TOADS to show a randomly constructed spritesheet each time you press Z or X.
I was originally planning on ending this blog post/write-up here. But instead it’s going to end where it all began, with PICO NIGHT PUNKIN’. Just this week, Chris and I returned to the game to release a Christmas update. This is something we had always joked about, and never seriously considered due to the size of the required sprites, but after everything I’ve done so far… we thought it was worth a shot.
The massive difference with PICO NIGHT PUNKIN’ is that we couldn’t possibly reconstruct a new spritesheet on initialization since we need around 4 spritesheets worth of sprites that will all be drawn on the screen in rapid succession. This means that we needed a way to draw sprites from spritesheets stored as strings alongside the sprites on the main spritesheet. To do this I created my own cart called
#FNF_SPRITE_CRUNCHER, which works fundamentally the same way that
#GFX_EXPORT does, turning a spritesheet into a string. However, it’s designed to export a single sprite by specifying it’s width and height before exporting. This exports shorter strings, and since it only exports what is needed from the sheet (the exact bounds of the sprite) it makes it much less intensive to iterate through the entire string each frame as needed. I’ve found that this allows me to draw up to ~3000 pixels worth of external sprites alongside the main spritesheet without dropping to 30fps, so we tried to cram as much as we could into the main spritesheet and left the sprites that would be swapped most frequently to the external sheets.
We’re really happy with how the final result turned out and the new update is completely open source on GitHub so feel free to see how everything works on the inside. We also ended up implementing the same method in the main menu so we wouldn’t have minor lag spikes when hovering over certain menu options. Hopefully this was a slightly comprehensive blog post on my experience working with multiple spritesheets in PICO-8 and can give you some insight on how to go about achieving similar results for yourself.
UPDATE: After writing this post, I stumbled upon this forum post which allows you to have up to 1024 sprites (4 spritesheets) by using their
cspr() function that “just works”. Check out the demo cart for yourself: