#pico-8
2023
- Pico-8 Cartridge Label for GPi Case
The GPi Case is a fancy case for the Raspberry Pi Zero that gives you a screen, speaker, buttons and battery in a design reminiscent of the original DMG-001 Classic Game Boy. I’ve got mine set up to emulate some old game consoles. It’s great. As the GPi Case uses a Raspberry Pi at its core, you can use it to run more than just game emulators. The Raspberry Pi plays Pico-8 games wonderfully (Pico-8 officially supports Raspberry Pi as a platform, too!). I first set Pico-8 up on a Pi with the Waveshare Game HAT back in 2019. I’d always wanted a dedicated Pico-8 console, so the GPi fits the bill perfectly. I bought a second GPi cartridge to make a dedicated Pico-8 cart for my GPi. It contains a Raspberry Pi Zero W and an 8GB Micro SD card with a stripped-down Retropie that boots straight to the Pico-8 splore menu so you can immediately find and launch Pico-8 games made by the community. I put these labels together using found assets and the Pico-8 logo. You can print them on paper and then just glue or tape them onto the cartridge case. Printed dimensions should be: Width: 39.5mm Height: 22.75mm
2019
- Pico-8 on the Raspberry Pi with Game HAT
I’ve been playing with Pico-8 a lot lately. I’ve got a small Pico-8 project site to launch soon, but in the mean time I’m more occupied with getting Pico-8 running on things.
- Approaches for a simple state system
There are lots of ways to achieve even the most simple goal in programming, and Pico-8 – even with it’s limited nature – is no different to any other system. Following on from my menu and state system, today I’m looking at state systems. There are lots of approaches to handling state out there, and each has their own pros and cons. In Pico-8 the main concern is keeping the token count low so that you can add more features to your game. Beginner state system This first state system uses a state variable and an enum table of available states to decide which state we’re currently in. The state variable is updated by user input (button press, or death) and it’s contents controls which functions are executed in the _update() and _draw() functions. If you’ve got a huge game the conditions in the _update() and _draw() functions can quickly become unweildly. The main benefit here is that you can control what to execute in the _update() and _draw() functions from one place. This approach is quite token hungry. function _init() t = 0 -- create timer state = 1 states = { menu = 1, game = 2, gameover = 3 } hp = 100 end function _update() t += 1 -- increment timer if state == states.menu then update_menu() elseif state == states.game then update_game() else update_gameover() end end function _draw() if state == states.menu then draw_menu() elseif state == states.game then draw_game() else draw_gameover() end end function update_menu() if btn(4) and btn(5) then state = states.game end end function draw_menu() print("Press z and x to start") end function update_game() if hp < 1 then state = states.gameover end end function draw_game() print("HP: "..hp) end function update_gameover() if btn(4) and btn(5) then state = states.game end end function draw_gameover() print("Press z and x to start over") end Improved state system This is the approach I prefer, the _update() and _draw() functions call a variable that points to another function. To change state you simply update this variable. There is no variable or enum needed to keep track of state, but you do need to explicitly redeclare the _upd and _drw variables each time you change state. This is approach is quite light on the token count. -- core init/update/draw functions function _init() t = 0 -- create timer hp = 100 _upd = upd_menu _drw = drw_menu end function _update() t += 1 -- increment timer _upd() end function _draw() cls() _drw() end -- menu update/draw functions function upd_menu() if btn(4) and btn(5) then _upd = upd_game _drw = drw_game end end function drw_menu() print("Press z and x to start") end -- game update/draw functions function upd_game() if hp < 1 then _upd = upd_gameover _drw = drw_gameover end end function drw_game() print("HP: "..hp) end -- gameover update/draw functions function upd_gameover() if btn(4) and btn(5) then _upd = upd_game _drw = drw_game end end function drw_gameover() print("Press z and x to start over") end
- Outlining text and sprites
It’s pretty simple to create an outlined text effect (as used in my menu system) with a simple function, such as: function printo(text,x,y,text_color,border_color) local text_color = text_color or 7 local border_color = border_color or 0 -- render the "outline" in each direction for _x=-1,1 do for _y=-1,1 do print(text,x+_x,y+_y,border_color) end end -- print the foreground text print(text,x,y,text_color) end -- usage: printo("Hello world!",64,64,12,8) The process for outlining a sprite is very similar, but it uses the pal() function to change colors and works best of all if your sprite is a single color… -- where spr_table is like: -- {spr_index, base_color} function spro(spr_table,x,y,spr_color,border_color) local spr_color = spr_color or 7 local border_color = border_color or 0 -- render the "outline" in each direction for _x=-1,1 do for _y=-1,1 do -- replace the base_color with new border_color pal(spr_table[2],border_color) -- draw the sprite with the new color spr(spr_table[1],x+_x,y+_y) end end -- replace the base_color with the new spr_color pal(spr_table[2],spr_color) -- render the recolored sprite spr(spr_table[1],x,y) -- reset the rendering palette pal() end -- usage: spro({1,7},64,64,12,8) And obviously you can set this spro function up to handle more of the properties available to spr, such as number of tiles in x/y, and whether to flip or not. As a final solution if you’re going to be doing this a lot you could combine the previous two functions to one mega outline function: function outline(what,x,y,fc,bc) if typeof(what) == "string" then printo(what,x,y,fc,bc) else spro(what,x,y,fc,bc) end end And here’s how the final effect looks when applied to a 1-color sprite and some text, with random colors:
- A menu and state system
I’ve been playing with state systems lately, and decided to build a menu system to play with going between each state. First of all, my helper functions… I’m using this to print outlined text: -- print text with an outline function printo(str,x,y,c1,c2) for xoff=-1,1 do for yoff=-1,1 do print(str,x+xoff,y+yoff,c1) end end print(str,x,y,c2) end The menu function has two modes, one for running in _update() and one for running in _draw(), you control this by passing "u" or "d" as the second argument. The first agument takes a table containing a menu definition. I’ll talk more about this with an example after the wall of code: function menu(m, mode) local ic=#m.items if mode == "u" then if btnp(2) then -- up m.selection -= 1 if m.selection < 1 then m.selection = 1 end end if btnp(3) then -- down m.selection += 1 if m.selection > ic then m.selection = ic end end if btnp(5) then -- x if m.items[m.selection][2] then m.items[m.selection][2]() end end end if mode == "d" then local padding={4,4,0,6}-- t/r/b/l local lineheight=8 -- find longest string local widest_str=0 for i,item in pairs(m.items) do if #item[1] > widest_str then widest_str = #item[1] end end local width = (widest_str*4)+padding[2]+padding[4] local height = (ic*lineheight)+padding[1]+padding[3] local xoff = 64 - (width/2) local yoff = 64 - (height/2) local x0 = xoff local y0 = yoff local x1 = xoff + width local y1 = yoff + height -- box rect(x0-2,y0+1,x1+2,y1+2, 5) rect(x0-1,y0-1,x1+1,y1+1,0) rectfill(x0,y0,x1,y1,1) rect(x0,y0,x1,y1,7) -- aditional text if m.guide then local _w = #m.guide*4 local _x = 64 - (_w/2) local _y = y1+padding[1]+2 printo(m.guide,_x,_y, 0, 7) end -- items for i,item in pairs(m.items) do if i == m.selection then -- selection highlight printo( item[1], xoff+padding[4], yoff+padding[1]+((i-1)*lineheight), 0,7 ) if m.items[m.selection][2] then palt(0,false) palt(1,true) local xadj=(5 - time()%3) spr(1,x0-xadj,y0+padding[1]+((i-1)*lineheight) - 1) pal() end else -- normal item printo( item[1], xoff+padding[4], yoff+padding[1]+((i-1)*lineheight), 0,6 ) end end end end There’s a lot going on there, but it’s basically maintain a counter for the user’s current selection, and then if there is a function associated with a menu item call it when the x button is pressed. Here’s how the menu definitions look _menu_start = { selection = 1, items = { {"new game", game}, {"new game", game}, {"new game", game}, {"new game", game}, {"credits", credits} }, guide="❎ to select" } _menu_game = { selection = 1, items = { {"game over", die}, {"congratulations", win}, {"---"}, {"back", start} } } _menu_credits = { selection = 1, items = { {"this sub menu contains lots of"}, {"strings without actions bound "}, {"to them, so you can just show "}, {"multiple lines of text."}, {"30 characters is a rough limit"}, {"---"}, {"back", start} }, guide="enjoy ♥" } You can go between menus using a state system. Here’s the barebones of a state system. We assign a new function to _upd() and _drw() each time the state changes. function _init() start() -- defined later end --update function _update() _upd() end function update_start() menu(_menu_start, "u") end function update_game() menu(_menu_game, "u") end function update_credits() menu(_menu_credits, "u") end --draw function _draw() cls(13) _drw() end function draw_start() menu(_menu_start, "d") end function draw_game() menu(_menu_game, "d") end function draw_credits() menu(_menu_credits, "d") end -- functions that are called directly -- through menu selections function start() _upd=update_start _drw=draw_start end function game() _upd=update_game _drw=draw_game end function credits() _upd=update_credits _drw=draw_credits end And here’s how it looks in action. I’ve got a custom sprite for the selection hand drawn in sprite slot 1:
2018
- A custom font
Out of the box Pico-8 has one font. It’s the utilitarian answer to most problems. 3px by 5px and fairly legible. This font isn’t always good enough. How about making your own font? I’ve seen lots of approaches for creating fonts in Pico-8 – mostly using sprites for each glyph of the font, but then you’re limited to 8x8px and a have to mess with pal modes if you want to adjust colors on-the-fly. My approach uses the raw pixel data encoded in strings that are rendered using calls to pset, falling back to the built-in font for any missing glyphs. First, here are my helper functions… -- merge tables function mt(t1,t2) local new = {} for k,v in pairs(t1) do new[k] = v end for k,v in pairs(t2) do new[k] = v end return new end -- less verbose way to do ternary operations function ternary(cond ,t ,f ) if cond then return t else return f end end I had to come up with a compact way to represent the glyphs. Initially I used something like: { glyph = "o", pixels = { 0,1,1,1,0, 1,0,0,0,1, 1,0,0,0,1, 1,0,0,0,1, 1,0,0,0,1, 1,0,0,0,1, 0,1,1,1,0, }} But that contains a lot of tokens and lots of repeated/dead values to represent the empty pixels in a glyph. Eventually I moved to something like: {"o", "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"} That’s the same pixel data, but encoded in to a much shorter string. The first number indicates our starting parity – this can be 0 or 1 and tells us if the top/left pixel is empty or filled. Each time we have a . the parity flips, so with the example of our “o” glyph it starts with 0 for 1 count, then 1 for 3 counts, 0 for 1 count, 1 for 1 count, etc. When the remaining pixels in a glyph are 0 we can just omit them from the glyph data as they’re not drawn. The real wins are in the more simple/empty glyphs, such as “.” for example. Before: { glyph = ".", pixels = { 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,1,0,0, }} After: {".", "0.32.1"} Making this change took me from an initial token count of roughly 2800 to a much more manageable count of 560-ish. So with that technical talk out of the way, here’s the complete glyph set in code, and the function to render text. I’ll give some usage examples after. global_y = 0 function write(str,opts) opts = opts or {} _opts = { x=0, y=global_y, c=7 } opts = mt(_opts,opts) font = { w=5, h=7, -- first num indicates starting value, 0 or 1 -- each number indicates number of 0 or 1 in a row -- flips at each . glyphs = { {"0", "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"}, {"1", "0.2.1.3.2.2.1.1.1.4.1.4.1.4.1.2.5"}, {"2", "0.1.3.1.1.3.1.4.1.3.1.3.1.3.1.3.5"}, {"3", "0.1.3.1.1.3.1.4.1.2.2.5.2.3.1.1.3"}, {"4", "0.3.2.2.1.1.1.1.1.2.2.3.6.4.1.4.1"}, {"5", "1.6.4.1.5.3.5.2.3.1.1.3"}, {"6", "0.1.3.1.1.3.2.4.4.1.1.3.2.3.1.1.3"}, {"7", "1.5.4.1.4.1.3.1.3.1.3.1.3.1"}, {"8", "0.1.3.1.1.3.2.3.1.1.3.1.1.3.2.3.1.1.3"}, {"9", "0.1.3.1.1.3.2.3.1.1.4.4.2.3.1.1.3"}, {"a", "0.1.3.1.1.3.2.3.7.3.2.3.2.3.1"}, {"b", "1.4.1.1.3.2.3.5.1.1.3.2.3.5"}, {"c", "0.1.3.1.1.3.2.4.1.4.1.4.1.3.1.1.3.1"}, {"d", "1.4.1.1.3.2.3.2.3.2.3.2.3.5"}, {"e", "1.6.4.1.4.4.1.1.4.1.4.5"}, {"f", "1.6.4.1.4.4.1.1.4.1.4.1"}, {"g", "0.1.3.1.1.3.2.3.2.4.1.1.4.3.1.1.3"}, {"h", "1.1.3.2.3.2.3.7.3.2.3.2.3.1"}, {"i", "1.5.2.1.4.1.4.1.4.1.4.1.2.5"}, {"j", "1.5.2.1.4.1.4.1.2.1.1.1.2.1.1.1.3.2"}, {"k", "1.1.3.2.2.1.1.1.1.1.2.2.3.1.1.1.2.1.2.1.1.1.3.1"}, {"l", "1.1.4.1.4.1.4.1.4.1.4.1.4.5"}, {"m", "1.1.3.3.1.3.1.1.1.2.3.2.3.2.3.2.3.1"}, {"n", "1.1.3.3.2.2.1.1.1.2.2.3.3.2.3.2.3.1"}, {"o", "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"}, {"p", "1.4.1.1.3.2.3.5.1.1.4.1.4.1"}, {"q", "0.1.3.1.1.3.2.3.2.3.2.3.1.1.3.5.1"}, {"r", "1.4.1.1.3.2.3.5.1.1.1.1.2.1.2.1.1.1.3.1"}, {"s", "0.1.3.1.1.3.2.5.3.5.2.3.1.1.3"}, {"t", "1.5.2.1.4.1.4.1.4.1.4.1.4.1"}, {"u", "1.1.3.2.3.2.3.2.3.2.3.2.3.1.1.3"}, {"v", "1.1.3.2.3.1.1.1.1.1.2.1.1.1.2.1.1.1.3.1.4.1"}, {"w", "1.1.3.2.3.2.3.2.1.1.1.2.1.1.1.2.1.1.1.1.1.1.1.1"}, {"x", "1.1.3.2.3.1.1.1.1.1.3.1.3.1.1.1.1.1.3.2.3.1"}, {"y", "1.1.3.1.1.1.1.1.3.1.4.1.4.1.4.1.4.1"}, {"z", "1.5.4.1.3.1.3.1.3.1.3.1.4.5"}, {" ", "0.35"}, {".", "0.32.1"}, {",", "0.27.1.3.1"}, {"!", "0.2.1.4.1.4.1.4.1.4.1.9.1"}, {"?", "0.1.3.1.1.3.1.4.1.3.1.3.1.9.1"}, {"'", "0.3.1.3.1"}, {"\"", "0.1.1.1.1.1.1.1.1"}, {":", "0.12.1.14.1"}, {";", "0.12.1.14.1.3.1"}, {"(", "0.3.1.3.1.3.1.4.1.4.1.5.1.5.1"}, {")", "0.1.1.5.1.5.1.4.1.4.1.3.1.3.1"}, {"[", "0.1.3.2.1.4.1.4.1.4.1.4.1.4.3"}, {"]", "0.1.3.4.1.4.1.4.1.4.1.4.1.2.3"}, {"{", "0.2.2.3.1.4.1.3.2.4.1.4.1.4.2"}, {"}", "0.1.2.4.1.4.1.4.2.3.1.4.1.3.2"}, {"<", "0.3.1.3.1.3.1.3.1.5.1.5.1.5.1"}, {">", "0.1.1.5.1.5.1.5.1.3.1.3.1.3.1"}, {"-", "0.16.3"}, {"+", "0.12.1.3.3.3.1"}, {"/", "0.4.1.3.1.4.1.3.1.3.1.4.1.3.1"}, {"\\", "1.1.5.1.4.1.5.1.5.1.4.1.5.1"}, } } _x = opts.x _y = opts.y letters = {} for i=1,#str do letter = sub(str,i,i) add(letters,letter) end for k,v in pairs(letters) do letter = nil for _glyph in all(font.glyphs) do if _glyph[1] == v then letter = _glyph[2] end end local _sx = _x local _sy = _y if letter != nil then local _px = _sx local _py = _sy pixels = {} local _p = sub(letter,1,1) local prev = nil for i=2,#letter do cur = sub(letter,i,i) if type(prev) == "number" and cur != "." then cur = sub(letter, i-1, i) end if cur == "." then _p = ternary(_p == "0", "1", "0") else for j=1,tonum(cur) do add(pixels, _p) end end prev = tonum(cur) end for i,p in pairs(pixels) do i = i - 1 if i % font.w == 0 then _py += 1 _sx = _x - (font.w * (i / font.w)) end _px = _sx + i if p == "0" then pset(_px, _py, opts.c) end end else font.w = 8 print(v, _sx, _y + ((font.h - 2)/2), opts.c) end _x += font.w + 1 end global_y += font.h + 2 end If you’re still with me, congratulations! Here is how you use the function: function _draw() cls() global_y = 0 -- reset on each draw call write("hello world") -- passing a color is possible! write("this is max 🐱", {c=12}) write("my font brings all") write("the devs to the yard") write("0123456789") write("abcdefghijklm") write("nopqrstuvwxyz") write(".,?!:;()[]{}-+/\\<>") write("yes, this is a lot") write("nicer to read! ♥") write("") write("█▒🐱⬇️░✽●♥☉웃⌂⬅️😐") write("♪🅾️◆…➡️★⧗⬆️ˇ∧❎▤▥") write("@#$%^&*") end And this is how it looks:
- A Dialog System
This is a flexible dialog system I’ve come up with. It has the following features: automatically wrap long lines of text automatically page super long text to additional dialogs delay control per-letter display First, here are some of the helper functions that I’m using… -- draw a box with a frame -- arguments: -- x,y position -- w,h dimensions -- p padding -- fg,bg foreground and background colors function box(x,y,w,h,p,fg,bg) rectfill(x,y,x+w,y+h,bg) rect(x+p,y+p,x+w-p-1,y+h-p-1,fg) end -- joins two tables function jt(table1,table2) local new = {} for k,v in pairs(table1) do new[k] = v end for k,v in pairs(table2) do new[k] = v end return new end -- sleep for a set number of seconds (using 30fps mode) function sleep(s) for i=1,s*30 do flip() end end And lastly, the meat of the dialog system. I’ve scattered some comments through-out the code to try to explain what I’m doing. I’ll provide some initialisation examples after… function dialog(d) d = d or {} -- defaults _d = { a = true, bg = 5, c = false, dl = 5, fg = 7, p = 8, t = true, ts = 0.05, txt = "", w = 128, y = 96 } -- slightly dynamic defaults _d.x = (128 / 2) - (_d.w / 2) _d.h = 128 - _d.y -- merge passed options with default _d = jt(_d, d) -- text _ch = 6 -- char height _wi = _d.w - (_d.p * 2) _wrap = (_wi / 4) - 1 -- 27 local lines = {} local limit = ceil(#_d.txt / _wrap) local _tb = _d.txt while limit > 0 do local substr = sub(_tb, 1, _wrap) -- grab first bit -- ensure line doesn't have a space at the start if sub(substr,1,1) == " " then substr = sub(substr, 2) end add(lines, substr) -- stick it in our lines table _tb = sub(_tb, _wrap + 1, -1) -- get rest of text limit -= 1 end -- lines limit _ll = flr((_d.h - _d.p - _ch) / _ch) if _d.t == true then -- type it out box(_d.x, _d.y, _d.w, _d.h, _d.p / 2, _d.fg, _d.bg) for l=1,_ll do local _l = lines[l] or "" -- vertical alignment local va = _d.y + _d.p + ((l - 1) * 6) local _lld = _ll while _lld > 1 do if #lines < _lld then va += (_ch / 2) end _lld -= 1 end -- center it if necessary local ha = _d.x + _d.p if _d.c == true then ha = _d.x + (_d.w / 2) - (#_l * 2) end local chars = {} local limit = #_l local ci = 0 -- split line in to chars while limit > 0 do local _ch = sub(_l, 1, ci + 1) add(chars, _ch) ci += 1 limit -= 1 end for c=1,#chars do print(chars[c], ha, va, _d.fg) if _d.a == true then sfx(0) end sleep(_d.ts) -- tiny delay end end else -- print normally box(_d.x, _d.y, _d.w, _d.h, _d.p / 2, _d.fg, _d.bg) for l=1,_ll do local _l = lines[l] or "" -- vertical alignment local va = _d.y + _d.p + ((l - 1) * 6) local _lld = _ll while _lld > 1 do if #lines < _lld then va += (_ch / 2) end _lld -= 1 end -- center it if necessary local ha = _d.x + _d.p if _d.c == true then ha = _d.x + (_d.w / 2) - (#_l * 2) end print(_l, ha, va, _d.fg) if _d.a == true then sfx(0) end end end if #lines > _ll then local _nl = lines -- delete strings we've used for i=1,_ll do del(_nl,lines[1]) end -- join thr remaining local str = "" if #_nl > 1 then for i=1,#_nl do str = str .. _nl[i] end else str = _nl[1] end sleep(_d.dl) -- delay before next -- call recursive local n = jt(_d, {txt=str}) dialog(n) else sleep(_d.dl) -- delay before next end end Whew that was a lot of code. Here’s how you can use it: -- short, centered, delay of 2 dialog({ txt = "hello,", c = true, dl = 2 }) -- short, centered dialog({ txt = "my name is inigo montoya", c = true }) -- short, centered dialog({ txt = "you killed my father", c = true }) -- short, centered, not typed dialog({ txt = "prepare to die!", c = true, t= false }) -- long, typed, multiple message boxes dialog({txt="this is a lot of text that will wrap on to multiple lines and hopefully use a recursive message box in order to show all of the text ♥"}) You can see how this all looks below: