Creativity through limitation: PICO-8 — Fantasy Console

Roman Petrov
The Startup
Published in
7 min readJun 18, 2019

--

It is the second article in “Creativity through limitation” series. Check out the first one: 8-bit demoscene. In this article, I’m going to tell you about the fantasy console PICO-8 and recreate two classic demoscene effects with it.

When was the last time you coded something just for fun? If you’re like me, then it hasn’t happened for years. However, about a year ago, I learned about PICO-8, bought it, and I have to say that these were probably the most worthy $15 I spent on myself last year!

What is PICO-8?

PICO-8 is a fantasy console for making, sharing and playing tiny games and other computer programs. When you turn it on, the machine greets you with a command line and simple built-in tools for creating your own cartridges and exploring the PICO-8 cartverse. — PICO-8 official page

Do you remember the era of 8-bit video game consoles and computers? PICO-8 returns you to those times. It is an “emulator” of a non-existing video game console with Lua “CPU” developed by Lexaloffle. It has a set of wisely designed constraints (speed, color palette, screen resolution, sound, and memory) which may look ridiculous, but it stimulates your creative muscle. Also, to let you start creating right away, PICO-8 gives you simple but convenient built-in tools: code, graphics, and sound/music editors.

PICO-8 has a fantastic community. Many people create games, demos, and tutorials. Teachers use it at schools to teach children programming. Professional game developers use it for experiments. One thing unites them all — it’s fun!

There’s also a pretty unique creative genre on PICO-8 — Tweetcart. Tweetcart is a program that fits in a Tweet (up to 280 characters) and produces some impressive visual effect. The idea is very similar to demoscene intro.

You can find more on Twitter by #tweetcart or at Museum of Tweetcart History.

Creating classic demoscene effects on PICO-8

If you don’t know what demoscene is, read the first article in this series. Many classic effects are recurring in different demos, and I’m going to re-create two of them on PICO-8: Vertical raster bars and Rotozoomer. PICO-8 uses the Lua programming language. If you‘re not familiar with it, it shouldn‘t be a problem, because Lua syntax is simple and straightforward.

PICO-8 can run in a browser. Try it!

Vertical raster bars

Let’s start with Vertical raster bars (also known as Kefrens bars, even though the first implementation was done not by Kefrens demo group). They look impressive on old computers because you could think — wow, they draw so many vertical lines! In reality, this effect doesn’t require drawing vertical lines at all. The classic implementation utilized Amiga’s ability to run some code for every scanline and change video memory address. You create a buffer for just one line and draw it repeatedly, adding a small “bar” every time in a different position.

You can use the same approach on PICO-8. There are no horizontal blank interrupts, but you don’t need it; instead, you use `memcpy` function. Let’s create a template first.

-- PICO-8 init function, create a coroutine here
function _init()
cfx = cocreate(fx)
end
-- The effect can run in 60 FPS, use _update60()function _update60()end-- PICO-8 frame draw function
function _draw()
-- Draw the frame
coresume(cfx)
-- Uncomment the following lines to see CPU load
--cursor(0, 0)
--color(15)
--print(stat(1))
end
function fx()
-- FX code will be here
end

I like to use coroutines for both drawing and updating state variables because they allow me to write code linearly and encapsulate all variables in a function. The downside of this approach is the slowdown if your drawing code occasionally takes more than one frame to complete. When you use _update() + _draw() approach, you only get frame rate drop, because PICO-8 always executes _update()/_update60() at the constant rate (30/60 times per second), but may call _draw() at the lower rate (30/15 times per second).

So, here’s the code with comments:

-- Raster bars
function fx()
-- Raster bars variables: phase counters
local ph1, ph2, ph3 = 0, 0, 0
local pht1, pht2, pht3, x
while true do
-- Clear the line buffer. I use the last line of the screen
-- for it to use PSET function to draw.
memset(0x7fc0, 0, 0x40)
-- Set temporary variables
pht1, pht2, pht3 = ph1, ph2, ph3
-- Loop over all 128 screen lines
for i = 0, 127 do
-- Vertical raster bars
-- 1. Calculate the position for the next bar piece
x = flr(63 + sin(pht1) * 19 + sin(pht2) * 9 + sin(pht3) * 7)
-- 2. Draw the bar by plotting points
pset(x - 1, 127, 0)
pset(x, 127, 5)
pset(x + 1, 127, 13)
pset(x + 2, 127, 13)
pset(x + 3, 127, 5)
-- 3. Update temporary phase variables for the next line
pht1 += 0.01
pht2 += 0.02
pht3 += 0.04
-- Copy the line buffer
memcpy(0x6000 + i * 64, 0x7fc0, 0x40)
end
-- Update phase counters
ph1 += 0.002
ph2 += 0.004
ph3 += 0.03
-- We're done, wait for the next frame
yield()
end
end

Now let’s add a roto-zoomer. I tried implementing a full-screen roto-zoomer on PICO-8 in 60 FPS and ran out of CPU time. However, if you combine a roto-zoomer with raster-bars, you don’t need to do it full-screen — You can draw in empty space only! It’s easy to track empty space with raster bars: the bar gets only wider every line, it can’t get narrower, so you can keep simply track the lowest and the highest X coordinates.

Roto-zoomer usually is a rotating and zooming picture, but I don’t want to deal with images in this example and will create a rotating and zooming checkerboard. Here’s the code:

-- Raster bars + roto-zoomer
function fx()
local ph1, ph2, ph3 = 0, 0, 0
local pht1, pht2, pht3, x
-- Roto-zoomer variables: rotation angle; zoom, X and Y phases
local zph, zzoom, zpx, zpy = 0, 1, 0, 0
-- Temporary variables
local xr, lx, hx, ztx, zty, zdx, zdy, zsx, zsy
while true do
memset(0x7fc0, 0, 0x40)
pht1, pht2, pht3 = ph1, ph2, ph3
-- Set roto-zoomer temporary variables
lx = 64
hx = 0
-- zdx, zdy — rotated and scaled step vector
zdx = cos(zph) * (1 + sin(zzoom) * 0.5)
zdy = sin(zph) * (1 + sin(zzoom) * 0.5)
-- zsx, zsy — position on the checkerboard
zsx = sin(zpx) * 20
zsy = sin(zpy) * 20
for i = 0, 127 do
-- Vertical raster bars
x = flr(63 + sin(pht1) * 19 + sin(pht2) * 9 + sin(pht3) * 7)
pset(x - 1, 127, 0)
pset(x, 127, 5)
pset(x + 1, 127, 13)
pset(x + 2, 127, 13)
pset(x + 3, 127, 5)
pht1 += 0.01
pht2 += 0.02
pht3 += 0.04
-- Roto-zoomer
-- 1. Update left and right boundaries
xr = flr(x / 2)
if (xr < lx) lx = xr
if (xr + 2 > hx) hx = xr + 2
-- 2. Coordinates at the beginning of the line
ztx = zsx + i / 2 * zdy
zty = zsy + i / 2 * zdx
-- 3. Draw left part
for c = 0, lx - 1 do
poke(0x7fc0+c,(band(bxor(zty, ztx),8) == 8) and 0xaa or 0)
ztx += zdx
zty -= zdy
end
-- 4. Update coordinates to skip drawn raster bar
ztx += zdx * (hx - lx + 1)
zty -= zdy * (hx - lx + 1)
-- 5. Draw right part
for c = hx + 1, 63 do
poke(0x7fc0+c,(band(bxor(zty, ztx),8) == 8) and 0xaa or 0)
ztx += zdx
zty -= zdy
end
-- 6. Clean some artifacts
if pget(x + 4, 127) == 10 then pset(x + 4, 127, 0) end
if pget(x + 5, 127) == 10 then pset(x + 5, 127, 0) end
-- Copy line
memcpy(0x6000 + i * 64, 0x7fc0, 0x40)
end
-- Update raster bars variables
ph1 += 0.002
ph2 += 0.004
ph3 += 0.03
-- Update roto-zoomer variables
zph += 0.001
zpx += 0.002
zpy += 0.003
zzoom += 0.001
yield()
end
end
Explanation of the main variables

Instead of using texture look-up, I use bitwise operations to create checkerboard. band(ztx, 8) gives a series of 0’s and 8’s every 8 pixels, bxor(zty, ztx) creates a checkerboard. To speed up the code and make both effects run in 60 FPS, I reduce the horizontal resolution and plot 2 points with a single poke (write a byte to memory) function instead of pset.

That’s it! You can find the full code and some other effect I create in my PICO-8 repository on GitHub.

As I recall, it’s the first time I’ve tried to explain a demo effect, and I’m afraid that it may not be clear enough. I tried to add as many comments as possible, but if you’ve got any questions, let me know, I’m ready to improve and add more details. Thanks for your attention!

I also post all my Medium articles to my personal website.

--

--

Roman Petrov
The Startup

Software developer and a music composer from Russia.