Technical write-up


The game is done, and we’re very pleased with its reception. Before I return to the real life I’ve been neglecting for the month of November, I wanted to post a quick summary of the less-straightforward effects we used in Dolan’s Cadillac. That this post will include a few spoilers for the game, so maybe go play it first before you read on; the game only takes a few minutes.

I’d never used PICO-8 until exactly a month ago today, so wizened experts shouldn’t expect any particularly deep insights here. Hopefully, those a few steps behind me on their journey will find something of value. On the other hand, I’ve been developing games on other platforms for 20+ years, so hopefully that’s worth something.

Palette Fades

At various points in the game, we fade the whole screen into or out of blackness. With an RGB pixel representation, this would be trivial, but palette-based graphics systems like PICO-8 don’t have a straightforward way to express “draw color X, but N% lighter/darker”. What you can do instead is change the actual color that each of PICO-8’s 16 palette entries maps to when they’re rendered to the screen using the pal(c0,c1,[p]) function, passing the value 1 as the optional p argument.

By remapping every color in the palette to a proportionally darker shade (or something in the neighborhood of that shade), you can make your whole screen look dimmer. When you use a different palette remap table every frame, you get an animated fade.

As I already noted, PICO-8 doesn’t give you an easy way to say “which supported color is most like C, but N% closer to some target color C1?”. Instead, you have to brute-force that calculation offline and generate a table of palette tables for each step in your fade.

Fortunately, somebody else has already brute-forced it and written a web tool to simplify the process. This tool lets you specify a number of fade steps (up to 16) and a target color, spitting out both a table of palette remap tables and the Lua code to apply the fade. It even supports PICO-8’s secret palette, which gives you 16 extra colors to play with, resulting in smoother overall fades.

That said, I didn’t use the tool’s output directly in Dolan’s Cadillac. Each remap table is 16 entries, so a 16-step fade requires almost 300 tokens just for the data alone, plus several dozen more for the code to apply each step of the fade. Instead, I wrote a simple Python script to postprocess the tool’s output table into a single giant string of hex digits – it was fewer characters overall, and only one token. (I also reordered the table elements a bit to make it easier for me to index into, but that’s just personal preference).

-- sample input
fade_table={1,2,15,3,...}
-- sample output
fade_table_str="01020f03..."

Each table element is exactly two characters in this string, and I know how many elements are in each fade table (16), so it’s easy to pluck each color out of the table using something like the following code:

tbl="01020f03..." -- the encoded table of fade tables
step=3 -- which group of 16 entries in tbl to use
b=1+32*step
for c=0,15 do
 pal(c,tonum(sub(tbl,b,b+1),1),1)
 b+=2
end

Large Static Images

After the player successfully buries Dolan and leaves him to his fate, the game ends with this lovely art c/o ConManEd.

Dropping this 128x128 image into the sprite sheet would…well, it would literally be the entire sprite sheet. Maybe we could save a bit by reusing tile for the large solid regions, but still, it would be a significant portion of the sprite sheet.

Instead, we use an approach that came to my attention by way of Johan Peitz, where large images can be carefully encoded as a text string and rendered using the humble print() function. The code to display the above image is literally:

print("⁶-b⁶x8⁶y8²0     ᶜ1⁶.\0\0\0\0\0\0ナ [continues for quite some time]",0,0)

Why does this “print” an arbitrary bitmap image? Because PICO-8’s print() command is bonkernanas.

Browse through the P8SCII reference, particularly the control codes. In addition to the usual crop of escape sequences (\n for carriage return, \t for tab, that sort of thing), PICO-8 strings can include commands to move the text cursor mid-string, change the color, size, and rendering mode, poke data into memory, define and play arbitrary sound effects… it could very well be Turing-complete.

For the purpose of displaying static images, the key feature is one-off characters, an escape sequence which lets you print an arbitrary 8x8 bitmap encoded in the string itself. By carefully crafting an 8x8 one-off character for each 8x8 region of the screen, moving the cursor into position to display each bitmap, setting the foreground/background colors appropriate, and possibly overlaying multiple bitmaps if a region requires more than two colors (all of which is possible using P8SCII control codes), the entire image is encoded in a single printable string.

Does that sound horrifically complex and error-prone? 100%, yes!

Did I actually do that myself? Absolutely not; I once again stood on the shoulders of giants. This post includes more details on the technique, and a link to a free web tool to convert arbitrary images to P8SCII strings.

Here’s the art that Donald fed into the stringifier to generate our victory screen as seen above:

Note that while the code to display the image is only 5 tokens long, the encoded strings can be massive; ours is over 6000 characters. Especially when combined with some of the other encode-data-as-strings tricks in this list, you may find (as we did) that the first size limit you hit is the ~16KB compressed-code ceiling, not the token or character limits.

Arbitrary sprite rotation

Coming from other fantasy consoles like TIC-80 and quadplay✜, one thing I missed in PICO-8’s API is the ability to draw a sprite at an arbitrary rotation. quadplay✜ supports arbitrary rotations, while TIC-80 only supports 90-degree rotations. But if you want even that much control in PICO-8, you need to draw four rotated copies of the sprite, using 4x as many precious sprite sheet slots.

Shoulders of giants to the rescue again! I found this thread with a 98-token implementation of sprite rotation with optional flipping and arbitrary scale:

function rspr(x,y,rot,mx,my,w,flip,scale)
 scale=scale or 1
 w*=scale*4
 local cs,ss=cos(rot)*.125/scale,sin(rot)*.125/scale
 local sx,sy,hx,halfw=mx+cs*-w,my+ss*-w,flip and -w or w,-w
 for py=y-w,y+w do
  tline(x-hx,py,x+hx,py,sx-ss*halfw,sy+cs*halfw,cs,ss)
  halfw+=1
 end
end

Note that since the code uses tline() to draw the sprite to the screen, the pixel data you’re drawing is read from the map region, not the sprite sheet. Just find an unused corner of your map and draw the sprite you want to rotated there, leaving a tile worth of padding on all sides.

We didn’t end up using this technique too much in Dolan’s Cadillac – just the car wheels and Dolan’s arm/gun. But the former is spinning too fast to really read visually as a rotation (we could probably just flip the sprite back and forth every frame to give the illusion of motion), and the latter is so small that we probably could have faked it with a few line() calls. But I’m sure the need to rotate sprites will come up again, and I’ll be glad to have this helper function in my proverbial toolbelt when the opportunity arises.

Engine Noise Volume Fade

With a bit of guidance from @ruin’s The Lawnmower Man, we put together a very satisfying engine noise loop in PICO-8:

I wanted to have the engine noise fade out when the car was offscreen (and fade in again when it arrives after the player digs their pit), but PICO-8’s controls for modifying the volume of an already-playing SFX are…well, they aren’t. You can ask the currently-playing music track to fade out with music(-1,[fade_ms]), but we already had a music track playing separately from the engine noise, and didn’t want the music to fade out along with the engine.

To solve this problem, we’re actually modifying the engine noise SFX in memory at runtime, adjusting the volume of each note in the loop. The data format of PICO-8 SFX is described here – basically, each note is a 16-bit value in memory, with the volume stored in bits 9-11. Eight levels of volume are supported for each note. However, we felt that the higher volume levels were too loud for the engine, and limiting ourselves to levels 0-4 resulted in a too-obvious stair-step quality rather than a smooth volume fade.

To give ourselves more distinct timbres to fade between, we also dynamically enable the per-SFX “dampen” effect as the sound gets quieter, as the effect applies a low-pass filter that simulates the effect of audio playing at a distance. Three dampen levels and eight volume levels give us 24 unique level+timbre pairs to choose from.

For extra credit I could have listened to each pair of parameter values and sorted them based on perceived loudness, but in practice I just naively set volume to level\8 and dampen to level\3. Here’s the final code to set the engine noise level:

-- adjust the engine volume
-- using a range from 0 to 23.
-- actually sets a combination
-- of volume and damping to
-- give a smoother gradient.
function engine_vol(lvl,sid)
 local bp,vol,damp=0x3201+sid*68,
                   lvl\3,2-lvl\8
 -- set damping
 local b=peek(bp+63)
 local det,rev=b\8%3,b\24%3
 poke(bp+63,(b&7)+8*det+24*rev+72*damp)
 -- set volume of all notes
 vol*=2
 for a=bp,bp+62,2 do
  poke(a,peek(a)&0xf1|vol)
 end
end

Speech Synthesis (?!?)

If you’ve finished the game (or found the easter egg hidden in the help screen), you surely noticed that the the cart can (in a manner of speaking) speak to you. If you’re wondering how this bit of dark Lua magic works…well, unfortunately, so am I!

Here’s the deal: I wanted a sound effect to accompany Dolan’s final words (“For the love of God, Robinson!”). I figured the PICO-8 could probably emit something like the indecipherable honking of the adults in old Charlie Brown cartoons (or Animal Crossing characters, if you prefer). So I did a quick web search for “PICO-8 speech”. to see if anybody had figured out how to configure an SFX to achieve this effect. Nothing obvious came up in the search results; instead, I found speako8.

This library uses a technique called Klatt synthesis, perhaps best known as “the voice of Stephen Hawking”. Integration into a PICO-8 app is pretty trivial: copy in a block of code, call its init/update functions from your own _init() and _update() functions, tweak the voice parameters to your liking, and then call say(str). The string needs to be pre-encoded into a particular markup syntax of phonemes and such, but (as usual) there’s a web app for that.

Under the hood, it’s using an undocumented (but also, in practice, quite thoroughly documented) feature of PICO-8’s serial() function to play PCM audio samples. Fortunately this uses a separate channel from sfx/music, so it plays well with your application’s other audio.

What’s the catch? Well, there are a few downsides:

  1. The audio samples are played at 5 KHz, which is not exactly CD-quality.
  2. Playing the audio consumes a significant amount of the console’s CPU budget, so there’s a good chance you’ll drop frames while speaking (particularly the frames which actually call say()). You can mitigate this by changing a buffer size deep in the code, but there’s still a noticeable hitch.
  3. The speako8 library code is around 1000 tokens, so that’s a lot of potential gameplay you’re giving up for some pretty questionable speech quality. For longer utterances, the encoded phoneme strings can grow quite large as well.

Once I found speako8, I was certain I wanted to incorporate it into the game, so I hid it in an include file and code-golfed mightily towards the end of the project to ensure that it fits. In the end I had to resort to a minifier to squeeze the cart into the code-size limits, so the speech effect is only available in the itch.io “party version” of the game. I hope it was worth it!

I’d love to dig into the guts of the library at some point, both to better understand how it works and to look for opportunities to save tokens and CPU cycles. It’s extremely flexible, and I bet most titles will only use a small fraction of the exposed functionality; ours certainly did. Maybe it would be possible to write a tool that lets you specify exactly which strings you want to speak and which parameters you want to customize, and outputs a stripped-down version of the library code that supports those specific use cases and no more?

Sounds like a project for another day.


That’s all I can think of that’s worth discussing. If there’s anything else you’re curious about, let me know in the comments and I’d be happy to elaborate!

Get Dolan's Cadillac

Comments

Log in with itch.io to leave a comment.

nice write up!

not sure to understand why the speak08 version is only for itch - if you were able to export the html, the png version will equally work.

You’re right of course, I can export the “party version” to any format. But my understanding is that we’re encouraged to post unminified carts to the BBS (open-source, encourage learning & all that), so I wanted to make sure there was a relatively readable build that was still 99% feature-complete.

a link to the source code will do - many games are using minification to get under limits.

Ooo, thanks, that’s great to know for next time! (Love your work btw!)