Jump to content
IGNORED

PICO-8 development


TehStu

Recommended Posts

Inspired by @bradigor talking about it on the Mental Health Gaming discord, I bought PICO-8 (and the early preview Voxatron) virtual console for the princely sum of $19.99. PICO-8 is available for just $14.99 on its own.

 

https://www.lexaloffle.com/pico-8.php

 

PICO-8 is basically a virtual implementation of a Spectrum-esque computer of old, and it comes with its own code, sprite, and music editor (which looks like an Amiga tracker). To give you a sense of what it can do, here's the spec:

 

Display 128x128 16 colours

Cartridge Size 32k

Sound 4 channel chip blerps

Code Lua

Sprites 256 8x8 sprites

Map 128x32 cels

 

If you've done any programming at all recently, you'll recognize the general syntax. I love how it's a mish mash of modern and new. It's limited, so you really have to think, but it has built-in functions to update the game state and draw the screen at 30 or 60 fps, so the game loop is already there for you.

 

Quote

PICO-8 cartridges can be saved in a special .png format and sent directly to other users, shared with anyone via a web cart player, or exported to stand-alone HTML5, Windows, Mac and Linux apps.

Any cartridge can be opened again in PICO-8, letting you peek inside to modify or study the code, graphics and sound.

 

If you share to HTML and host it somewhere (your own webspace, or itch.io by the sounds of it) you even get on-screen touch controls when accessing via a mobile device.

 

Just to fart around and get the lay of the land, I created this:

 

rllmuk_logo_0.gif.aef3bf11cc41399512093743a0d493e7.gif

 

// draws the rllmuk logo

// calc dot size based on 
// bubble radius
function sizes()
  ps = rb / 2
  rs = rb / 4
end  

// set bubble start size
function _init()
  rb = 20
  sizes()
end

// resize bubble on keypress
function _update()
  // increase if p1 btn = 
  if(btn(5) and rb<64) then 
    rb += 1
    sizes()
  end
  
  // decrease if p1 btn = 
  if(btn(4) and rb>5) then
    rb -= 1
    sizes()
  end
end

// draw the logo
function _draw()
  cls(7)
  
  // purple bubble
  circfill(64,64,rb,1)
  rectfill(64,64,64-rb,64+rb,1)
  
  // cyan dot
  circfill(64,64-ps,rs,12)
  
  // pink dot
  circfill(64+ps,64,rs,14)
  
  // yellow dot
  circfill(64,64+ps,rs,10)
  
  // green dot
  circfill(64-ps,64,rs,11)
end

 

You can fire up PICO, hit Esc to get into the code editor, copy/paste the above in, hit Esc again and type RUN to see it working. Player 1 fire buttons are X and C to scale up and down. It scales a bit weird once you're down to small sizes but that's a limitation of rounding.

 

Anyway, that was my first program and took about 15mins. SO much fun. I've got a million ideas for where to go next.

 

Anyone fancy joining in and making daft games? (hint hint @moosegrinder)

Link to comment
Share on other sites

Oh, and a wrote a game that fits in a tweet:

 

cls()x,y,s,d,r=64,64,0,2,rnd::a::
if(r()<.2)circ(r(127),r(127),r(4),8)
if((btn(0)or d==0)and x>0)x-=1d=0
if((btn(1)or d==1)and x<127)x+=1d=1
if((btn(2)or d==2)and y>0)y-=1d=2
if((btn(3)or d==3)and y<127)y+=1d=3
if(pget(x,y)==8)goto e
pset(x,y,10)s+=1flip()goto a::e::
?s

 

Link to comment
Share on other sites

30 minutes ago, deKay said:

FWIW, PICO-8 was in that massive itch.io bundle a while back, so chances are you already have a copy.

 

Oh wow, didn't even realize. Seems I already owned it via 'Bundle for Racial Justice and Equality'. So yeah, hit up itch.io folks, you may already own this.

Link to comment
Share on other sites

I enjoy making Tweetcarts this time of the year :)

 

Tweetcarts are as they suggest - enough code to do something interesting in 140 (now 280) characters - a bit like the equivalent of the 1k/4k demo scene.
Well worth a scan through on Twitter - people have done amazing things with them!

 

 

Big caveat: It's a cute exercise, but I absolutely wouldn't want anyone to look at Tweetcart code and think Pico-8 is too complicated- these are technical exercises... creating something that looks good and then squashing it up in an unnatural way. General Pico-8 development is much more accessible, and I'd highly recommend it to anyone who wants to have fun learning or playing with code. It's a *very* well designed little toy box.

Link to comment
Share on other sites

4 hours ago, bradigor said:

Where is the best idiots tutorial? I want to give this a go 

Great question, I need to start finding resources. I haven't dug very far yet, just reading the official stuff. Want to find something though, think I'll introduce my kids to this stuff.

 

Complete aside, for anyone who has done Lua coding before - is this the best way to structure objects? I'm assuming back in the 8bit days you'd just have a million separate variables, but that feels a bit weird. So I thought I'd experiment with nested tables:

 

pizza1 = {
  ["type"] = "cheese",
  ["size"] = "large"
}

pizza2 = {
  ["type"] = "pepperoni",
  ["size"] = "small"
}

order = {}
add(order, pizza1)
add(order, pizza2)

for pizza in all(order) do
  print(pizza.type)
  print(pizza.size)
end

 

prints:

Quote

cheese

large

pepperoni

small

 

This feels like a reasonable way to do it, but perhaps I'd better read up on Lua (or PICO's implementation) before I go down weird paths.

Link to comment
Share on other sites

Yep, looks fine to me. You only need to wrap the keys if they aren't straightforward, i.e. you should be able to just write:

 

pizza1 = {
	type = "cheese",
	size = "large"
}

 

(unless type and size are keywords... it's been a while since I did this!)

 

You can also do a lite version of OOP. I believe Pico-8's version of Lua is pretty vanilla, so generic docs will help you through.

Link to comment
Share on other sites

One of the great things about Pico-8 is that there's a large library of things that people have made, and you can break in and tinker with the code, make small changes and see what they do. A great way to learn. There is (or at least there was when I was last involved) a nice supportive community forum too.

Link to comment
Share on other sites

On 22/12/2021 at 10:56, yakumo said:

All this looks amazing but all these code is making my brain melt

No worries man. The PICO is based on Lua which seems to be a mish-mash of so many languages that learning ANY will help you here, and learning this will help you pick up other stuff. Makes me wish I picked up Lua back in the day, pretty sure that's what WoW interface mods were based on.

 

Anyway, I've refactored my logo code a bit to make it easier to read. Here's the whole thing:

 

Spoiler
-- interactive rllmuk logo
-- by @tehstu, 2021

-- calc dot offset and radius
function calc_dots()
  dot_o = r / 2 -- offset
  dot_r = r / 4 -- radius
end  

-- set logo start size
function _init()
  r = 20 -- radius of bubble
  calc_dots()
end

-- update logo on keypress
function _update()
  -- inc radius if p1 btn = 
  if(btn(5) and r < 64) then 
    r += 1
    calc_dots()
  end
  
  -- dec radius if p1 btn = 🅾️
  if(btn(4) and r > 5) then
    r -= 1
    calc_dots()
  end
end

-- draw the logo
function _draw()
  cls(7)
  
  -- purple bubble
  circfill(64,64,r,1)
  rectfill(64,64,64-r,64+r,1)
  
  -- cyan dot
  circfill(64,64-dot_o,dot_r,12)
  
  -- pink dot
  circfill(64+dot_o,64,dot_r,14)
  
  -- yellow dot
  circfill(64,64+dot_o,dot_r,10)
  
  -- green dot
  circfill(64-dot_o,64,dot_r,11)
end

 

 

Let's go through it line by line.

 

-- interactive rllmuk logo
-- by @tehstu, 2021

Anything with -- is just a comment. Stuff PICO ignores comments, they're purely to make code more readable. However, when you export your game from PICO to a PNG (for easy sharing on the web), it'll take these first two comments and slap them on the PNG preview. If you look at deKay's posts there, it's the white writing at the bottom of the cart. Nifty!

 

Now I do this:

-- calc dot offset and radius
function calc_dots()
  dot_o = r / 2 -- offset
  dot_r = r / 4 -- radius
end  

The whole point of this daft code is to resize the rllmuk logo by holding fire button 1 or 2. As the size increases or decreases, I need to work out where the little colored dots go and how big they should be. I actually have the original SVG (and the file that created it) for the rllmuk logo, so I could cheat a bit. I eyeballed the dots to be roughly half way along the radius of the purple bubble, and about half the radius of the bubble in size.

 

So, this function, which I "call" every time the user presses a button, does the following:

  • let the current dot offset be equal to half the radius of the bubble, such that the dots are centered along the radius
  • let the current dot radius be a quarter (so a half in total) of the bubble radius

It's not exactly that but eh, I wasn't going for accuracy and we're talking about a 128x128 pixel grid here. It's good enough maths. Why did I put this in a function? Because it's a chunk of code that repeats frequently. I didn't have to, in fact I didn't at first, but I scoped the duplicate code out of the main part of the code and stuck it in a function later. Do or don't, all good! It just helps readability and bug hunting later.

 

Now what?

-- set logo start size
function _init()
  r = 20 -- radius of bubble
  calc_dots()
end

_INIT() is a very special function. It's called exactly once when you run your game. So here, I do this:

  • set the initial radius, stored in a variable called R, to 20 pixels
  • call our calc_dots function to figure out how big the dots should be and where they should be

 

Next up:

-- update logo on keypress
function _update()
  -- inc radius if p1 btn = 
  if(btn(5) and r < 64) then 
    r += 1
    calc_dots()
  end
  
  -- dec radius if p1 btn = 🅾️
  if(btn(4) and r > 5) then
    r -= 1
    calc_dots()
  end
end

This is another special function, called _UPDATE()

 

The PICO runs everything in this function 30 times a second, or 60 if you put _UPDATE60() instead. This is the part of the so-called "game loop" where we change stuff based on what's going on.

 

So first, I'm doing an IF to check if button 5 has been pressed. How did I know which button? If you have a look at this cheat sheet here, you can see that the X button for Player 1 is button 5. So I'm checking if Player 1 has pressed down the X button. But also you'll see an AND in there, looking to see if R, is less than 64 pixels. Recall that the screen is 128 pixels wide, and you'll see later I slapped the logo in the middle, so to stop it being made larger than the screen I set the maximum radius to half the width of the screen, basically.

 

Now, what IF button 5 is pressed AND the Radius is less than 64 (pixels)? Well,

  • R += 1 is shorthand for R = R + 1. Basically, make the Radius equal to itself plus 1. Make it one bigger. We started with 20 so now it's 21, for example.
  • Second, we call CALC_DOTS(). I mean, we resized the logo, so we need to figure out the new position and size of the dots, right?

 

Now, if none of that was true, we next check to see IF player 1 pressed button 4 AND the Radius is greater than 5. Why? Weird scaling issues happen any smaller than that, so I set an arbitrary limit. IF either of those are true, we:

  • R -=1, or subtract 1 from Radius. IF it was 20, it's now 19.
  • Second, we figure out the size and position of the old dots again. See how sticking that in a function saved us repeatedly putting the dot maths throughout the code?

So 30 times a second, the game is asking itself... did they press button X or O, and is the Radius a particular size? If so, make the Radius slightly bigger or smaller. That's it, that's the "game engine", if you like.

 

But now we actually need to draw something:

-- draw the logo
function _draw()
  cls(7)
  
  -- purple bubble
  circfill(64,64,r,1)
  rectfill(64,64,64-r,64+r,1)
  
  -- cyan dot
  circfill(64,64-dot_o,dot_r,12)
  
  -- pink dot
  circfill(64+dot_o,64,dot_r,14)
  
  -- yellow dot
  circfill(64,64+dot_o,dot_r,10)
  
  -- green dot
  circfill(64-dot_o,64,dot_r,11)
end

 

Luckily, PICO has a built in way of drawing the screen automatically 30 frame per second (or 60), called _DRAW(). There's some clever guff going on behind the scenes with a display buffer (what the user is literally currently seeing) and a draw buffer (what the game is drawing before displaying the whole thing as a frame to the user). As long as your game and drawing code take less time than a frame, it'll run 30 (or 60) fps. Automatically. Huzzah.

 

So what am I doing?

 

CLS(7) clears the screen. That's Spectrum BASIC function, :lol:. However, I'm also throwing a 7 in there which is light grey. I got that from the cheat sheet, top right.

 

Now the rest is just geometry. CIRCFILL() draws a filled circle. The numbers you give CIRCFILL, arguments, tell it following info in the following order:

  • 64: this is the x position on the screen. In an X,Y coordinate system, 0,0 is top left and 127,127 bottom right. See the cheat sheet. so x=64 puts it slap in the middle on the X axies
  • 64: this puts it slap in the middle of the Y axis (as best you can in an EVEN numbered coordinate system). So that's our circle slap in the middle of the screen
  • R: this tells it the radius, specifically the value in our variable R. At the beginning, it'll be 20 pixels. Thereafter depends on which buttons you're hitting!
  • 1: the color 1, or what looks to me like purple

Now I draw a purple filled rectangle in the bottom left corner of the circle using RECTFILL(). To draw a rectangle, you give it the X,Y of one corner (in this case, 64,64, the middle of the screen) and then the X,Y of the opposite corner. Here's some maths now. I set the opposite corner's X to 64-R, or 64 minus the circle's Radius. The Y is 64+R, 64 plus the circle's radius. I got this wrong to start with, bloody thing ended up top left instead of bottom left. Anyway, that combination of circle and square very approximately creates the speech bubble. Close enough!

 

Next up, the 4 colored dots. I'll just explain one, as they're all the same thing, in essence. I'm again using CIRCFILL() to draw a filled circle. The cyan dot, which is towards the top of the purple bubble, is positioned thus:

  • 64: the X position is the middle of the screen still, like the bubble itself
  • 64-DOT_O: the Y position is up a bit. How much up? Well, we calculated how many pixels up in the function CALC_DOTS(). It set the offset, DOT_O, to Radius / 2. Half way along the radius. So at the start of the game, the Radius is 20 pixels and so DOT_O is 20 / 2 = 10. So 64 - DOT_O is 54. Y = 54.
  • DOT_R: the little dot needs a radius, and CALC_DOTS() worked out DOT_R to be Radius / 4. At the start, this is 20 / 4 = 5. So the dot radius is 5, or half the bubble radius
  • 12: the color cyan, again from the cheat sheet

 

Now I just repeat all this 3 times for the other 3 dots. All I needed to do was vary the X,Y position by adding or subtracting DOT_O (the dot offset) as needed, and setting the appropriate color.

 

That's it. When you run this, it does the following:

  • Calls _INIT() which sets the initial bubble size
  • Calls _UPDATE() to see if we pressed any keys
  • Calls _DRAW() to paint the logo
  • Now it calls _UPDATE() to see if we pressed any keys
  • Now it calls _DRAW to paint it again, whether we pressed keys or not
  • etc. until you quit

Hope this helps. It's about the dopiest thing I could write but at least made me think about some maths. Happy to help with any of this sort of explainer. It doesn't matter if all the jargon goes over your head, there are way better tutorials out there for explaining this I'm sure. I just wanted you to see how, when broken down, it's not as "what the hell is all this code" as it might appear.

Link to comment
Share on other sites

Here's a zip with hopefully not-borked versions of loads of things I've fiddled with over the years. Not much finished, but a variety of things!

 

pico.zip.pdf

 

(rename it to .zip obviously)

 

Oh, and I blogged about some of these things as I went along:

 

https://lofi-gaming.org.uk/blog/category/game-dev-diary/

 

Link to comment
Share on other sites

17 hours ago, deKay said:

Here's a zip with hopefully not-borked versions of loads of things I've fiddled with over the years. Not much finished, but a variety of things!

Those work! Invaders is excellent. I hope I can make things half as polished. Also I enjoyed my corn beef sandwich, cheers.

Link to comment
Share on other sites

  • 3 weeks later...

I've been busy with this (caught mid game, I thought the GIF would record the whole thing):

 

ling8_3.gif.72c081093db8613b372412734824de5f.gif

 

As you can see, I've got a bug in my "check for letters which are in the word but are in the wrong position" code, because it flags R as grey instead of yellow.

 

This was a fascinating exercise. I got a little way in before I realized PICO-8 doesn't have string input! So I came up with that silly keyboard at the top. There's a total lack of polish (i.e., no playing another game without running cart again), it needs to be functional first. I did a bunch of stuff which was total overkill, like derive all the colors from a palette so that you could change it, if you wanted to (high visibility, dark mode, etc.). Again though, I started making these nice to have features before I realized - how the heck do you allow those options with such limited input from the user? So I guess I'll need to eventually add stuff to the pause menu.

 

I don't think I'll work directly in PICO-8 again for the code. It made my brain melt working with such a narrow screen, to the point I wrote crappy code just to make variables fit. But eventually I gave up and started letting lines scroll off the screen. I completely take my hat off to 8bit coders, on the reasonable assumption there were essentially no dev tools back in the day. But yeah, I think I'll just #INCLUDE SOMECODE.LUA in future and use VScode to work on the file directly.

 

Another cool thing I learned is using github to sync my code back and forth my laptop and Pi. I use the Pi during the day, and didn't just want to Dropbox sync it or whatever. Speaking of the Pi, as you run PICO-8 from a terminal by default, you have a terminal handy to output debugging stuff via PRINTH(). Obviously, wasn't quite that simple for me on Windows because I moved the code location from app data to somewhere more convenient. Here's a page to help with PRINTH on each machine, though: https://pico-8.fandom.com/wiki/Printh

 

edit - oh, and I guess you can just keep adding words to it until you run out of memory, cart space, or patience. It's just an array, currently with 1 word, and picks one at random at runtime. I love that I had to write my own function for "does this character exist within this string" but PICO already came with a function to randomly select a value from a table if you pass RND() a table instead of the usual range for the random number (0 to x).

Link to comment
Share on other sites

Yay! Stuck my son in front of it (and helped him through because of the bug with yellow letters) and he thought it was awesome and wants to play. He even said I should randomize the words so I could play and I was like I have! But the list of randomly pulls from is 1 word.

 

Told him you can upload to itch.io and bosh, now he's interested in making PICO games.

 

Edit - it's based on wordle, in case you've been living under a rock :)

Link to comment
Share on other sites

21 minutes ago, TehStu said:

This was a fascinating exercise. I got a little way in before I realized PICO-8 doesn't have string input! So I came up with that silly keyboard at the top.

 

Looks like it's possible to take keyboard input (though, obvs, PICO-8 is intended to run on devices without, and I like your solution because it also does the letter highlight thing). Did you try anything like this?

 

21 minutes ago, TehStu said:

I don't think I'll work directly in PICO-8 again for the code. It made my brain melt working with such a narrow screen, to the point I wrote crappy code just to make variables fit. But eventually I gave up and started letting lines scroll off the screen. I completely take my hat off to 8bit coders, on the reasonable assumption there were essentially no dev tools back in the day. But yeah, I think I'll just #INCLUDE SOMECODE.LUA in future and use VScode to work on the file directly.

 

I might be misunderstanding what you're doing, but you can find the PICO-8 user folder somewhere and just edit in your favourite editor. As long as you remember to save before running then it's easy enough! I've written small things in PICO-8's environment and it isn't fun to do :D

Link to comment
Share on other sites

1 minute ago, Skykid said:

I might be misunderstanding what you're doing, but you can find the PICO-8 user folder somewhere and just edit in your favourite editor. As long as you remember to save before running then it's easy enough! I've written small things in PICO-8's environment and it isn't fun to do 

Oops, I didn't realize the p8 files were plain text! I guess I'll do that. Although, I suppose VScode knows it's Lua with the appropriate file extension.

 

Keyboard input! Interesting. Didn't think to look for ways folks have extended functionality.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue. Use of this website is subject to our Privacy Policy, Terms of Use, and Guidelines.