I’ve been consistently trying to learn game development with the Pico-8 virtual console for about a month.
I’ll ask for help with a practice game project here soon, but anyone can use this thread to discuss Pico-8. :)
I’ve been consistently trying to learn game development with the Pico-8 virtual console for about a month.
I’ll ask for help with a practice game project here soon, but anyone can use this thread to discuss Pico-8. :)
Good luck! I can’t help with it, but many of my favorite games and tech demos are made in PICO-8. Even owned a license for a while, years ago, gifted by a friend. It didn’t work out.
Looking forward to seeing what you create. I haven’t used Pico-8 before but was just a week ago reading up on it. I’ve dabbled with Love2D before which also uses Lua. I have some time off work next week so keen to have a play with Pico-8😊
--tab 1--
function _init()
px=20
py=92
flp=false
pf=1
i_bubbles()
end
function _update()
moveduck()
u_bubbles()
end
function _draw()
cls(-13)
map()
aniduck()
d_bubbles()
end
--tab 2 duck--
function moveduck()
if btn(⬅️) then
px-=1
flp=false
elseif btn(➡️) then
px+=1
flp=true
end
if px<0 then
px+=1
elseif px>120 then
px-=1
end
end
function aniduck()
if pf>2.9 then
pf=1
--tab 3 bubbles--
function i_bubbles()
bubbles={}
tmr=0
for b=1,3 do
add(bubbles,{
x=rnd(120),
y=rnd(50),
sx=rnd(2)-1,
sy=.5
})
end
end
function u_bubbles()
tmr+=1
if tmr%90==0 then
i_bubbles()
end
Here’s what I’ve done so far. Now I want to make it so that when the duck touches a bubble, I gain a point. I’ve done similar things while following along with tutorials, but I just can’t generalize it so far… Can anyone familiar with Lua help me?
Edited to add: I have the bubble sprite set to sprite flag 0.
Your query prompted me to download Pico-8 this evening and start learning how it works.
You can create multiple ‘tabs’ to separate the logic of your game. Here Manatee has separated their code across three tabs for the game loop, the code for the player sprite (a duck), and the code for bubbles sprites, respectively.
Common Pico-8 functions are _init() called once at game start, _update() called every frame, and _draw() also called every frame. Here Manatee is abstracting these into each tab by calling custom functions i_bubbles(), u_bubbles() and d_bubbles() respectively. You could otherwise just call the necessary functions directly within _init(), _update() and _draw(), but this is personal preference.
What’s happening here is a duck, controlled by the player, and bubbles are being displayed when the game starts. A timer regularly adds more ducks. Manatee wants to update a score when the duck touches the bubbles
In other game engines, there’s usually a concept of colliders or areas that you can check for an overlap between. However, in Pico-8 it seems you need to roll your own collision detection, whether that be with walls, pickups or interactibles like your bubbles (Your duck might not be technically picking up bubbles, but the principle is the same)
I want to make it so that when the duck touches a bubble, I gain a point
Poking around in the demos and some YouTube tutorials, there are several ways you could approach this. Caveat that i’m still learning Pico-8 myself; do refer to the linked resources:
[ Edit: since writing this I’ve realised that your bubbles might be moving as you said ‘catching bubbles’ in the chat. If that’s the case, and your bubbles arent static, the bounding check approach might be best. ]
[ Edit 2: added more examples and fixed the formatting ]
This is probably the simplest approach, but I think it’s the most restrictive since it relies on positions in the sprite sheet (i.e. if you accidentally change the graphics, you might be checking against the wrong item).
This works by checking every game loop whether the tile under the player sprite uses a certain sprite, then replacing it. You could also assign points here. You can see an example using apples in the Wander demo.
score=0
function u_bubbles()
if (mget(x,y)==10) then -- if map value at player coords matches bubble sprite number
mset(x,y,14) -- replace the bubble sprite in the map (I.e. bubble is popped)
score+=1 -- update score
sfx(0) -- play sound effect
end
end
This works by checking if the tile the player is about to move to has a relevant flag set. You can see an example in Dylan Bennett’s (MBoffin) Adventure Game series. The relevant video is step 5, about picking up keys.
I’ve adapted it here to just use the current tile, but i’d recommend checking out their video where they use the adjacent tile as this allows you to check for walls too. I’d generally recommend the video series as its well explained and they have a very logical game structure.
score = 0
function i_bubbles()
bubble = 0 -- naming the flag for easier readability in the code
end
function moveduck()
…
interact(x,y) -- check if the tile at this position has a bubble
end
function interact(x,y)
if (is_tile(bubble, x,y)) then -- check there’s a bubble at this tile
pop_bubble(x,y) -- replace the tile if there is (and add points)
end
end
function is_tile(tile_type,x,y)
tile=mget(x,y) -- get the tile at the player's position
has_flag=fget(tile, tile_type) -- check if it has the specified flag
return has_flag -- return true if it does
end
function pop_bubble(x,y)
score+=1 — increase points
swap_tile(x,y) -- replace the bubble tile with one without a bubble
sfx(1) -- play a sound effect
end
function swap_tile(x,y)
tile=mget(x,y) -- get the tile at this position on the map
mset(x,y,tile+1) -- swap the tile for the next one in the sprite sheet
end
This works by checking the distance between the player sprite and an object and, if it’s within a defined range, acting on this. You can see an example in SpaceCat’s video. I’ve adapted this to work with your table (array) of bubbles, and i’ve used the active/not active approach SpaceCat has used as presumably that’s safer than removing objects from a table that is being looped, but do check out their example.
score=0
function i_bubbles()
bubbles={}
...
add(bubbles,{
x=rnd(120),
y=rnd(50),
active=true, -- added this; used to control which bubbles are rendered
...
}
end
function u_bubbles()
for b in all(bubbles) do -- for each of the bubbles
if b.active then
if abs(px-b.x)<=4 and abs(py-b.y)<=4 then -- check distance by subtracting the bubble’s z position from the player’s x position and rounding it off
b.active=false -- stop checking for this bubble
score+=1 -- increase score
end
end
end
function d_bubbles()
for b in all(bubbles) do
if b.active then
spr(10,b.x,b.y,2,2) -- draw bubbles only when active
end
end
print(“score”..score , 10, 10, 9) -- print the current value of 'score'
end
This would be useful if your sprite movement isn’t tile based. It uses a bounding box for collision detection. There’s an example in Pico-8 zine #3 used for bullet detection and a simpler example by morningtoast in the Pico-8 forum. Again, a very common approach I’ve seen in other game engines.
I’ve simplified it here by assuming you’re not interested in creating a bounding box smaller than the player/bubble sprite like the bullet example. It works just by comparing each object’s ‘box’ (calculated based on their x/y position plus width/height) to check if they overlap.
player={
x=10 * 8, -- multiplied by 8 so that the coordinates are pixel values (to match enemies and collision check)
y=4 * 8,
w=8, -- sprites are 8px by default in Pico-8
h=8
}
bubble={
x=10 * 8,
y=3 * 8,
w=8,
h=8
}
function collide(obj, other_obj)
-- if the x position of one object is less than the x position plus width of another object (and same for y and height) then they overlap
if
obj.x < other_obj.x + other_obj.w and
other_obj.x < obj.x + obj.w and
obj.y < other_obj.y + other_obj.h and
other_obj.y < obj.y + obj.h
then
return true
end
end
function _update()
for bubble in all(bubbles) do
if collide(player, bubble) then
print(“collided”)
end
end
Honourable mention, included for completeness, not sure it’s ideal here (can a smaller collectible be missed if its not directly under a corner?), but this is a common approach for collision detection for hazards and walls.
This works by getting the coordinates of the player, based on the left, right, up, down edges, then checking if there’s a tile under the corners of those edges that has a flag set.
You can see an example in SpaceCat’s YouTube video, based on this function by Scathe in the pico-8 forums.
function collide(player)
-- store edges
local X2=player.x/8
local y1=player.y./8
local x2=(player.x+7)/8
local y2=(player.y+7)/8
-- if any of the corners overlap with a tile with flag 0 return true
local a=FGET(MGET(x1, y1),0)
local b=FGET(MGET(x1,y2),0)
local c=FGET(MGET(x2, y2),0)
local d=FGET(MGET(x2, y1),0)
-- if any of the corners returned true, pass this back to the function that called this
If a or b or c or d then
return true
else
return false
end
end
I’ve never dabbled in fantasy/virtual consoles myself, but I’m glad it’s making people practise writing games with limitations even if they’re arbitrary. They say boundaries make creativity flourish, after all.
I guess it also appeals to those who miss the old days of gaming but don’t want to invest in buying retro hardware or games, which is valid.
The only fantasy console that caught my interest personally is the Dreambox, which is a fantasy console that is trying to mimick the power of the Dreamcast and PS2!
I’m trying to use the bounding box method but keep getting syntax errors.
--tab 1--
function _init()
px=20
py=92
flp=false
pf=1
score=0
iduck()
i_bubbles()
end
function _update()
moveduck()
u_bubbles()
for bubble in all(bubbles) do
if collide(duck,bubble) then
print "collided"
end
end
end
function _draw()
cls(-13)
map()
aniduck()
d_bubbles()
print(score)
end
function collide(obj,other_obj)
if obj.x < other_obj.x + other_obj.w and
other_obj.x < obj.x + obj.w and
obj.y < other_obj.y + other_obj.h and
other_obj.y < obj.y + obj.h
then
return true
end
end
--Duck (tab 2)--
function iduck()
duck={
x=10*8,
y=4*8,
w=8,
h=8
}
end
function moveduck()
if btn(⬅️) then
px-=1
flp=false
elseif btn(➡️) then
px+=1
flp=true
end
if px<0 then
px+=1
elseif px>120 then
px-=1
end
end
function aniduck()
if pf>2.9 then
pf=1
else
pf+=.1
end
spr(pf,px,py,1,1,flp)
end
--Bubbles (tab 3)--
function i_bubbles()
bubble={
x=10*8,
y=3*8,
w=8,
h=8
}
bubbles={
}
tmr=0
for b=1,3 do
add(bubbles,{
x=rnd(120),
y=rnd(50),
sx=rnd(2)-1,
sy=.5
})
end
end
function u_bubbles()
tmr+=1
if tmr%90==0 then
i_bubbles()
end
for b in all (bubbles) do
b.x+=b.sx
b.y+=b.sy
end
end
function d_bubbles()
for b in all(bubbles) do
spr(17,b.x,b.y)
end
end
Hello, what syntax errors are you getting?
When I try to run your code I get an attempt to perform arithmetic on field 'w' (a nil value)
syntax error.
This is because, when you initialise a bubble you include the w (width) and h (height) variables:
bubble={
x=10*8,
y=3*8,
w=8,
h=8
}
However, when you later add a bubble to your bubbles table, you don’t:
add(bubbles,{
x=rnd(120),
y=rnd(50),
sx=rnd(2)-1,
sy=.5
}
Since the collision code relies on these properties, the program crashes. You need to ensure that all your objects have the w and h properties.
However, that’s not the only issue. Before I explain, you might find it helpful to draw some debug boxes to see where your colliders are. Add the following to one of your tabs (tab 0, for example):
debug_mode = true
function _draw() -- your existing _draw() function
if debug_mode then
draw_hitbox(duck, 11) -- duck hitbox in light blue
for b in all(bubbles) do
draw_hitbox(b, 8) -- bubble hitbox in red
end
end
end
function draw_hitbox(obj, col)
if not obj then return end
local x = (obj.x or 0) + (obj.collider_x or 0)
local y = (obj.y or 0) + (obj.collider_y or 0)
local w = obj.collider_w or obj.w or 8
local h = obj.collider_h or obj.h or 8
rect(x, y, x + w - 1, y + h - 1, col or 8) -- default color is red
end
This draws a rectangle around any objects passed to it.
You’ll notice that this works for the bubbles, but the rectangle for the duck is nowhere near the duck. This is because the duck sprite is drawn and moved using px
, py
but your hitboxes are being drawn based on the object x
, y
(i.e. duck.x
, duck.y
).
I’d recommend using only one set of coordinates, such as the object x
, y
, for both to keep it simple (you could otherwise assign px, py to the duck’s x,y variables so they are in sync). In other words:
function moveduck()
if btn(⬅️) then
duck.x-=1
flp=false
elseif btn(➡️) then
duck.x+=1
flp=true
end
if duck.x<0 then
duck.x+=1
elseif duck.x>120 then
duck.x-=1
end
end
Also, update the way the sprite is drawn in animduck():
spr(pf,duck.x,duck.y,1,1,flp)
You can then remove any references to px and py.
Also, you might notice that you don’t see the “collided” statement when collisions happen. This is an oversight on my part: print()
will only show when called within a _draw() function. We can still trigger it from the collision code in _update by first setting a variable and printing that variable in your _draw() function. You should also clear the print statement so that it can be triggered each time a collision happens:
msg_timer=30 --30 frames
function _update()
if collide(duck,bubble) then
msg = "collided"
msg_timer=30 --reset timer
end
-- clear the msg after a short delay
if msg_timer>0 then
msg_timer-=1 -- count down the timer
else
msg="" -- clear the timer
end
end
function _draw()
print(msg, 20, 20)
end
However, we might aswell update your score, since that’s what you originally wanted to do.
You cant just increment the score, we need to stop checking collisions for that bubble after the first collision (otherwise the score will keep incrementing). There’s a couple of options here:
If you want to ‘pop’ the bubbles, you can add to the score and remove the bubble:
for bubble in all(bubbles) do
if collide(duck, bubble) then
del(bubbles, bubble) -- remove the bubble
score += 1 -- increment the score
break -- important to stop the loop after deleting to avoid errors
end
end
If you otherwise want the bubbles to stay on screen, you can set a variable so that the collision check stops after the first collision:
bubble={
x=10x8,
y=3*8,
active=true -- to control bubbles to be checked for collisions
}
for bubble in all(bubbles) do
if bubble.active and collide(duck, bubble) then
active=false -- exempt this bubble from further collision checks
score+=1
end
end
Maybe I’d better just start over. The code is quite a mess. I might find top-down adventures easier to start with than arcade-style games.
That’s completely understandable — I get why it might feel like a mess. I can’t tell you how many times I’ve felt so lost in a project that starting over seemed like the only option.
That said, you’ve already got some really cool stuff working. Rather than scrapping everything, I’d love to help you make sense of what you’ve built. Before you start from scratch, I’d suggest trying a quick refactor — not to overhaul your game, but to give you a clearer foundation to build from.
Here are a few ideas that might help:
——
Try grouping related parts of your code together (e.g. player, enemies, utilities) so each tab serves a single purpose. You’ve already started doing this — I think the debug code may have just made it feel more tangled. Moving that to its own “utilities” tab can help.
Tip: Add a comment at the top of each tab explaining what it’s for.
⸻
I can see you’re using functions like i_bubbles() and u_bubbles() to group your game logic — that’s a common Pico-8 pattern. But especially early on, it can help to stick with the _make, _update, and _draw functions and clear function names so it’s easier to tell at a glance what’s happening where.
Tip: Keep using _init(), _update(), and _draw() as your “main loop” and call your other functions from there. It gives you a clean top-level view of your program and you can easily comment out functions to turn whole parts off when tracking down bugs.
⸻
Short variable names like b, pf, or sx are fine once the code is working — but when debugging, longer names (like bubble, player_frame, speed_x) make things much easier to follow.
Tip: Choose a naming pattern — for example, prefix sprites with spr_, or positions with pos_ — so you can tell what things are at a glance.
⸻
Whenever you write something that might confuse you a week from now, add a comment. I do this constantly — it helps me think through logic, spot mistakes, and keep track of half-finished ideas.
⸻
Before jumping into the code, describe what your function should do in plain English. It gives you a checklist to follow, and helps make sure the logic makes sense before debugging starts.
⸻
I definitely don’t want to rewrite your project — that wouldn’t help in the long run. But would it help if I put together a simple boilerplate template? It wouldn’t include any actual game logic, just a structure with comments that you can fill in.
You’ve already got all of the relevant code, it would just be repurposing it to fit a structured template which will help you better understand what it’s doing. Would that help?
It might help. Thanks. :)
Okay, here you go.
I’ve seperated your program into 5 tabs:
I intentionally split the level/map code to a seperate tab as you might want to have multiple levels in future and this just keeps your main game loop clean.
I’ve completed the code for tab 0 and tab 4 so you can see how these could be wired up. The code for tabs 1 through 3 just needs to be added.
You already have this code, but hopefully my comments give you a hint as to what parts to use (happy to walk through individual functions if you get stuck)
I’ve also made some small but intential design changes:
I’ve used consistent naming for the functions in the main game loop. This means you don’t have to call different functions for different entities within there, you can take care of this in their individual tabs (e.g. the update_player() function handles movement and animation, but the update_bubbles() function only currently handles movement)
I’m seperating update logic and visuals where possible. This avoids confusion and means one isn’t dependant on the other.
I’ve renamed your ‘duck’ to player. This is optional but does mean your code is more portable to other projects. You could do the same for the bubbles, if you want.
I’m coding defensively to catch errors: for example, checking I have the expected items being passed into a function before I start acting on them, and providing default values for parameters if they’re omitted
You can probably safely copy and paste the template code into your program, but I would comment out your existing code so that you still have it, ready to refer to, adapt and redistribute into this template. You can comment out multiple lines by using a block comment:
--[[
for bubble in all(bubbles) do
if collide(duck, bubble) then
msg = "collided"
end
end
]]
I would recommend trying to implement one feature at a time (e.g. the level, the player, the bubbles etc), and one function at a time within those features (e.g. the setup code, the display code, the movement code etc).
Obviously it goes without saying that you don’t need to follow my approach exactly if you don’t want to. I just think that, by setting things up like this in a way that’s hopefully more organised, you might have a better chance of understanding all the moving parts, and will then be in a better position to change or improve it where you want to.
-- game loop
function _init()
setup_level()
setup_bubbles()
setup_player()
end
function _update()
update_player()
update_bubbles()
if debug_mode then
update_message()
end
end
function _draw()
display_level()
display_player()
display_bubbles()
update_ui()
-- if debug_mode is true (found in tab 4)
if debug_mode then
-- display a single hitbox for the player
display_hitbox(player, 11)
-- loop through and display multiple hitboxes as there are multiple bubbles
for obj in all(bubbles) do
draw_hitbox(obj, 8)
end
-- draw any messages sent by print statements
draw_message()
end
end
-- player code
function setup_player()
-- define your player object with variables for:
-- pos_x, pos_y, width, height, frame and is_flipped status
end
function update_player()
-- call functions to move the player, update player animations, and check for collisions
end
function move_player()
-- detect input (left/right buttons)
-- update player position
-- set the is_flipped status
end
function update_player_animation()
-- increment the player frame
end
function display_player()
-- draw the player sprite using the current player frame
end
function check_player_collisions()
-- check for collisions between the player and all bubbles
-- remove the collided bubble and exit loop
-- increment the value of the score variable
-- optional: call the show_message helper function show a "collided" message
end
-- bubble code
function make_bubble()
-- define the properties of a single bubble with variables for
-- pos_x, pos_y, width, height, speed_x, speed_y, sprite
-- tip: return just these variables, *not* a table of these variables
end
function setup_bubbles()
-- define an empty bubbles table
-- initialise the bubble timer
-- loop through the number of bubbles you want to create and
-- populate bubbles table by calling make_bubble() when you add each bubble
end
function update_bubbles()
-- calls function to move bubbles
-- could optionally call a function to animate bubbles
end
function move_bubbles()
-- increment the bubble timer
-- if the timer reaches its limit
-- call setup bubbles again
-- loop through all bubbles and
-- add the speed x/y to the bubble's position
end
function display_bubbles()
-- loop through all bubbles and
-- draw the bubble sprite at the current position
end
-- game/level code
function setup_level()
-- set any initial level variables
-- for example, set the initial value of the score
end
function display_level()
-- clear the screen to a chosen background colour
-- optionally draw the map (note: this only has an effect if tiles are placed in the map editor, which you might already be doing)
end
function collide(obj,other_obj)
-- check we've received an object, otherwise return to prevent errors
-- check for collisions between obj and other_obj
-- return true if there is a collision
end
function update_ui()
-- print the score
end
-- helper utilities
--set the intiial state of debug_mode
debug_mode=false
-- variables for messages to print and how long to display them for
msg=""
msg_timer=30
-- a single hitbox (used for one-off objects like the duck or a single bubble
function draw_hitbox(obj, col)
-- check we've received an object, otherwise exit to prevent errors
if not obj then return end
-- get coordinates and with/height to use for the collider
local x = (obj.x or 0) + (obj.collider_x or 0)
local y = (obj.y or 0) + (obj.collider_y or 0)
local w = obj.collider_w or obj.w or 8
local h = obj.collider_h or obj.h or 8
-- draw a rectangle at the object's location using these values
rect(x, y, x + w - 1, y + h - 1, col or 8) -- default color is red
end
-- displays the given message for the given duration
function show_message(text,duration)
-- store the given text
msg = text
-- set the timer to the provided value (or a default if not provided)
msg_timer = duration or 30
end
function update_message()
-- if the there's still time left on the timer
if msg_timer > 0 then
-- reduce the value of the timer
msg_timer-=1
else
-- when the timer runs out, reset the message to an empty string
msg=""
end
end
function draw_message()
-- so long as the message is not empty
if msg != "" then
-- print the message at the given coordinates, with the given colour
print(msg, 20, 0, 7)
end
end
I hope that makes sense. Do feel free to ask if you still have questions.
function setup_player()
player={
x=10,
y=95,
w=8,
h=8,
frame=1,
is_flipped=false
}
end
function update_player()
move_player()
update_player_animation()
check_player_collisions()
end
function move_player()
end
function update_player_animation()
if player.frame>1 then
player.frame=1
else
player.frame+=1
end
end
function display_player()
spr(player.frame,player.x,player.y,is_flipped)
end
function check_player_collisions()
end
Okay, why isn’t my player sprite appearing?
Hello,
So a couple of things:
Where are you calling display_player()?
function display_player()
spr(player.frame,player.x,player.y,is_flipped)
end
This is the function that draws your sprite on screen. Assuming your player sprite is in position 1 on the spritesheet, this should display your player. However, if this function never gets called, the player sprite will never appear.
For example, i’d expect there to be a display_player()
call in Tab 0 _draw() function.
Second, you’re looping through your animation frames too fast:
function update_player_animation()
if player.frame>1 then
player.frame=1
else
player.frame+=1
end
end
This code says: if the current animation frame is greater than 1, reset it to 1. Otherwise, increase it by 1.
Since your animation starts on frame 1, the first time it runs, it increases to 2. On the next frame, player.frame
is now greater than 1, so it resets to 1. As a result, your animation toggles rapidly between frames 1 and 2.
This might sound fine, but in PICO-8, the game runs at 30 frames per second (FPS), so this means you’re switching between animation frames 30 times per second — much too fast for the player to perceive. It will look like flickering or no animation at all.
You previously you had code like the following:
function update_player_animation()
player.frame += 0.1
if player.frame > 2.9 then
player.frame = 1
end
end
This worked better because it changed the frame more gradually. Since you’re increasing frame
by 0.1 each update, it takes 10 frames (a third of a second) to go from 1.0
to 2.0
, so each sprite stays on screen for 10 game frames before switching — creating a smooth 3 FPS animation.
To put it another way: PICO-8 runs at 30 FPS, so each frame lasts 1⁄30th of a second. If you want your animation to play at 10 FPS, you need to hold each sprite for 3 game frames (because 30 ÷ 10 = 3).
One way to control this is by using a step
counter — increment it every frame, and only update the animation when the counter reaches a certain value (like every 3 frames). Here’s an example:
function setup_player()
player = {
frame = 1,
step = 0
}
end
function update_player_animation()
player.step += 1
if player.step % 10 == 0 then -- change sprite every 10 frames (3 FPS)
player.frame += 1
if player.frame > 2 then
player.frame = 1
end
end
end
This line..
if player.step % 10 == 0 then
..means if what is left over after the step is divided by 10 is equal to zero then.. In other words, every 10 frames, do the thing that follows this.
This approach gives you precise control over the animation speed. Just change the number in step % N == 0
depending on how fast you want the animation to run:
3
= 10 FPS (30 / 10 = 3)6
= 5 FPS (30 / 5 = 6)10
= 3 FPS (30 / 3 = 10)Okay, I think I need to start with nothing but a movable player sprite and a sprite that acts as a wall.
And then learn collision with just those two bare-minimum things before trying to put multiple moving parts together.