These Doodle Insights are brought to you by my super generous patrons on Patreon!
In Pico-8 you can EASILY write to the sprite-sheet at runtime. Let’s take advantage of that!
One of the most basic functions of Pico-8 is ‘spr()’, which lets you display any of the 256 sprites from the spritesheet. You can also use ‘sspr()’ to display any part of the spritesheet directly. Displaying sprites is a basic feature in any game engine, but Pico-8 has more. Pico-8 can read and write to its spritesheet at runtime, easily and super-fast! This allows for many great tricks and rendering cheats and all sorts of cool stuff!
So much cool stuff in fact that this will be the subject of several parts, maybe just two, probably three!
Today, in this first part about reading and writing to the sprite-sheet, we’re gonna see the different ways to write to the sprite-sheet along with some practical examples!
Our first way to write to the sprite-sheet is the ‘sset()’ function! This function simply lets you change the color of any pixel on the sprite-sheet by writing ‘sset(x,y,c)’. It is by far the simplest way to modify the sprite-sheet, as it takes coordinates directly and it will make sure that those coordinates are not outside of the sprite-sheet.
In this doodle, the sprite-sheet is used as a secondary screen, where some custom cellular automata is happening! That cellular automata is done using ‘sget()’ to get the state of cells and ‘sset()’ is used to change them! Then, the sprite-sheet is used as map for the procedural dithering happening directly on screen, once again using ‘sget()’.
(if you want to learn more about cellular automata, check out these Doodle Insights, and if you don’t know what procedural dithering is, I have written three Doodle Insights on this subject too)
Here again, custom cellular automata is happening on the sprite-sheet, which is then presented on the screen through some sort of procedural dithering. (yeah, cellular automata can totally simulate sand physics if you know how to hide the little anomalies ;D)
Something more interesting this one is the state of the screen at the start. On the previous doodle, the starting sprite-sheet was a black canvas with random specks of colors and that was generated with code. In this one, it is more interesting to design the starting sprite-sheet yourself by going into the sprite editor and place the pixels yourself! This way, the original sprite-sheet is data that can be exploited and modified at runtime!
Yet, even if ‘sset’ is super easy to use, it is also quite slow and will only modify one pixel every call. It works well for cases where you’re treating pixels individually but it’s not ideal if you want to actually draw things on the sprite-sheet.
The ideal would be to have for the sprite-sheet the same drawing functions as for the screen. Two options here: either code your own sprite-sheet drawing functions or use ‘memcpy’!
‘memcpy(adst, asrc, len)’ will copy the portion of ram memory of the pico-8 of the length len and starting at asrc, to adst. It’s ok if you didn’t get that.
Say you have one line of apples and then one line of pears. Doing ‘memcpy(start_of_apple_line, start_of_pear_line, line_length)’, you duplicate your line of pears and replace the line of apples with the duplicate. (also, the apples don’t exist anymore)
--memory: 1,2,3,4,5,6,7,8 memcpy(0, 4, 3) --memory: 5,6,7,4,5,6,7,8 for i = 0, 2 do memcpy(i*2, 6, 2) end --memory: 7,8,7,8,7,8,7,8
Hopefully this didn’t confuse you even more!
The reason I’m trying to explain you how ‘memcpy’ works is that the ram memory happens to be where both the screen data and the sprite-sheet are stored. Therefore, we can do a ‘memcpy’ from the screen to the sprite-sheet!
If you want to know more about the memory layout of the Pico-8 ram and the memory functions, I encourage you to read the ‘Memory’ section of the pico-8 manual! For now, all you need to know is that the sprite-sheet’s position is 0x0000 (that’s hexadecimal for 0) and the screen is at 0x6000 (that’s also hexadecimal) and they both have a length of 0x2000. (which is hexadecimal for 128*64 and yes that’s the whole screen, explanation is further below)
So here’s what we do! We draw whatever we want on the screen using our handy Pico-8 drawing function, including lines, circles and text, and then we ‘memcpy’ the screen to the sprite-sheet using the following line.
memcpy(0x0000, 0x6000, 0x2000)
(you can actually use decimal values if you want to, and 0x0000 can be replaced by 0x0 or just 0)
Here is one of my favorite doodles! In this example, all the generation is done directly on screen, using mostly lines and psets. But when the generation is all done, I wanted to animate some parts of our scene, such as the water, the lava and the ores.
“Luckily”, those elements have different colors than everything else. However, animating these areas would completely mess up those colors. What do we do? We use ‘memcpy’! Just after the generation is over, we save the screen to the spritesheet with ‘memcpy(0x0, 0x6000, 0x2000)’ and then we animate the screen with custom cellular automata and procedural dithering (those again) based on the data in the sprite-sheet!
Another one of my favorite doodles! If you read the last Doodle Insights on Pico-8 voxel tech, you already know that the voxel data is taken from the sprite-sheet and that cells of a certain color, the screen color, are replaced by the display of the game played on the TV, which is also stored in the sprite-sheet.
I can’t remember how it was originally but I can tell you that for the tinyTVjam base cart, I wanted to be able to use the Pico-8 drawing functions on the game’s display. That’s why at the very beginning of each frame, the TV game is rendered on the screen, then copied to the sprite-sheet, then covered up by the scrolling background and then looked-up in the sprite-sheet by the voxel rendering!
But we also have important stuff in the sprite-sheet, such as the background and possibly sprites for our TV game! The tiny-TV screen is only 10×11 pixels. Let’s not copy the whole screen. Let’s just copy those 10×11 pixels!
for y=0, 10 do memcpy(i * 64, 0x6000 + i * 64, 5) end
Why 64? Why 5?? We’re coming to it.
This code copies the tiny game display to the sprite-sheet, line by line, without modifying anything else in the sprite-sheet!
‘memcpy()’ is my favorite way to put things in the sprite-sheet, just because it allows me to use all the Pico-8 drawing functions. It’s also fairly fast, but you have to make sure you get your memory addresses right or you may not like the result.
But what if I told you that there was yet another way to modify pixels of the sprite-sheet? A way super fast but also hard to use and useful only in very specific cases? I’m of course talking about ‘poke()’!
This is about to get very technical. If you’re not interested, feel free to skip to the conclusion.
Now seems a good time to tell you about the way Pico-8 stores its pixels in the ram! You may think that every byte of the screen data and the sprite-sheet data represents one pixel. That is not the case. Each byte represent TWO pixels!
See, a byte is 8 bits and Pico-8 has 16 colors, treated as indexes from 0 to 15. Any number from 0 to 15 can fit in just 4 bits. Thus, a byte can optimally contain 2 colors. And indeed, in each “pixel byte”, the 4 first bits represent the right pixel of a pair and the remaining 4 bits represent the left pixel of that same pair.
Using bitwise operations offered by Pico-8, we can have a formatted pair of pixel this way:
byte = bor(left_color, shl(right_color, 4))
Not using bitwise operations, you can get the exact same result with:
byte = left_color + right_color * 16
If you don’t understand why that is, you’re really dumb and life’s gonna get you. Just kidding it’s totally fine, I didn’t understand it either at first and besides it’s some pretty useless knowledge nowadays anyway.
But once you have that byte value, you can ‘poke’ it right into the screen at the wanted position using:
adst = y * 64 + x / 2 + 0x6000 poke(adst,byte)
Are you even still reading?
Remove the ‘0x6000’ from ‘adst’ and you’re poking your byte into the sprite-sheet! 64 bytes is one complete line of pixels on the screen. (or on the sprite-sheet) Your x coordinate has to be an even number, otherwise it gets even more complicated.
Surprise! I never actually used ‘poke’ on the sprite-sheet in any of my doodles! The reason for this is that it’s complicated to use ‘poke’ properly. (as you may have understood by now)
It’s ok though because our good friend Jakub Wasilewski (follow him) has a few articles about a lighting effect that makes really good use of the ‘poke’ function and I’m very happy to link it here: Part 1, Part 2, Part 3, Part 4! The first part should be enough to get how he’s using ‘poke’. Of course if you like the first part you should totally read the other three as well, they’re very good! (they’re not really about the sprite-sheet though, sorry)
‘poke’ looks cool on the paper but it’s actually highly unpractical and useful only in rare cases where you need to process a lot of pixels at once without dropping frames. I think you should try to use it just to put what you just read to the test and then keep it in a corner of your mind just-in-case.
‘sset’ is the easiest way to modify the sprite-sheet at runtime and it’s particularly great when you want to modify individual pixels but it’s not-so-great when you want to do actual drawing operations! In that scenario, ‘memcpy’ lets you draw whatever you want to the screen and then store it in the sprite-sheet, just make sure you get you copy the right things to the right places! ‘poke’ is super fast but it’s pretty complex to use. It can still come in handy in need of very fast operations!
That’s it for this first part on reading and writing to the sprite-sheet, I hope you enjoyed it! For our second part next week, we’ll be diving into more complex uses of ‘sset’ and ‘memcpy’, such as layering and generated animations!
If you have any questions or remarks, feel free to post them here, on the Patreon post or on Twitter!
The Doodle Insights series is possible thanks to the support of my awesome patrons on Patreon, whose names are written below!
Joseph White, Adam M. Smith, Ryan Malm, Matthew, Giles Graham, Luke Davies, Jake Meanwell, Tim and Alexandra, Sasha Bilton, berkfrei, Nick Hughes, Christopher Mayfield, Jearl, Dave Hoffman, Thomas Wright, Morgan Jensen, Zach Bracken, Cole Smith, Paul Nicholas, Anne Le Clech, Flo Devaux, babyjeans, Emerson Smith, Cathal O’Keeffe, Dan Sanderson, Andrew Reist, vaporstack, Dzozef, Jared Butowsky, Tony Sarkees and Justin TerAvest!
Thank you for reading and have fun with the Pico-8 sprite-sheet!
This is really good, and helped me get my code working for this idea – one correction I noticed though in the peek/poke section:
LikeLiked by 1 person
That’s right! Thank you for that, and also I’m glad this write-up helped you!