Coding in PICO-8 is done in a lightweight and easy-to-learn language called Lua. It’s quick, powerful, and is by far the most popular scripting language in game development today, having been used in everything from Dark Souls to World of Warcraft. So even if you’re just a little bit interested in game dev, it’s a good skill to have. This tutorial will walk you through using Raspberry Pi and PICO-8 to make a simple retro space-shooter, a great foundation for things to come.
This tutorial was written by Dan Lambton-Howard and first appeared in The MagPi issue #84. Get a free Raspberry Pi computer with a 12-month subscription to The MagPi.
See also: PICO-8 for Raspberry Pi starter guide
PICO-8 for Raspberry Pi: Launch sequence initiated
First things first, launch PICO-8 and, from the console, hit ESC. You should now be staring at the code editor. It isn’t the most beautiful text editor, but you’ll sure grow to love it! We want to start with a blank slate, so if you already have a cart loaded you might need to reboot in the console. Before we start with the code, two things to note: PICO-8 doesn’t use upper case letters, everything is lower case (so hands off that Caps-Lock). Secondly, similar to Python, there is no need for semicolons to end lines.
The holy trinity of PICO-8
PICO-8 has three special functions that structure any PICO-8 program. The first, _init(), is run once at program startup, whilst _update() and _draw() are called 30 times a second, meaning games are 30 fps by default. Define these three functions in your code, as in Figure 1. You can also give your game a title by using -- to comment. We’ve chosen something suitably B-movie for our retro space-shooter. Hit ESC to return to the terminal and type save yourgamename to save your cart (you should do this often), then ESC again to hop back to the code editor.
Ready Player One
No space-shooter is complete without a solitary pilot flying a super-advanced experimental warfighter. Switch to the sprite editor (using the tabs at the top right) and draw our ship. Don’t worry too much about graphics as we’ll be covering that in a later tutorial. Doodle a spaceship facing right in sprite slot 001. Write the following code into your _init() function to declare the player as a table: player = {x=20,y=64,sprite=1}. Tables are very useful in Lua; this one contains a reference to your player’s x and y coordinates, as well as what sprite to draw.
Moving the player
Now, within the _draw() function, add cls(). Then, on a new line, add
spr(player.sprite,player.x,player.y). This will tell PICO-8 to clear the screen each frame, then draw the player at the x and y coordinates stored in the player table. You can test this by hitting CTRL+R. You should see your little ship on the screen. Now let’s get them moving! The following code placed in the _update() function should move the player when the direction keys are pressed.
if btn(0) then player.x-=2 end
if btn(1) then player.x+=2 end
if btn(2) then player.y-=2 end
if btn(3) then player.y+=2 end
The enemy reveal themselves
But what are we fighting against? Those evil green blobs from outer space, that’s who! Draw a suitably alien-looking creature in sprite slot 002. We want our enemies to be attacking in waves. You can see the full code in the source, but briefly we are declaring a new empty table in init() named enemies. Then we write a new function createenemies() which creates a new enemy (similar to how we created the player) and then adds it to the enemies table. Lastly, a new function create_wave() spawns a number of enemies.
They’re coming for us!
To actually draw the enemies, we need to write a for loop in draw(). This loops over all the enemies in our enemies table, once per frame, and draws them on the screen. Aliens are no threat if they just sit there, so we need them to come towards the player. A simple way of doing this is to write another loop in _update()that alters each enemy’s x value per frame. Now let’s actually spawn some. Add createwave(rnd(6)+5) into _init(). This will call our enemy wave function that we wrote earlier, and create five to ten aliens on startup.
Our pilot strikes back
Run your game and you should be immediately swarmed by aliens. We need some way of fighting back! Let’s code some lasers. We do this in a very similar way to enemies, by declaring an empty lasers table, making a new function to create a laser, and writing a for loop to update each laser’s position, and one to draw each laser (as a red rectangle). The difference is we add if btnp(4) then create_laser(player.x+5,player.y+3) end after our player movement. This creates a new laser in front of the player when they press X (or B button on a controller).
High-speed collision detection in PICO-8
You’ve probably noticed that our lasers are entirely ineffective against the alien scum. That’s because we haven’t coded any collision detection. There are many ways to do this – entire books have been written about the topic – but let’s keep things simple. We’ll declare a new enemy_collision() function that checks if a point is inside an 8×8 pixel square around an enemy. If so, it returns true. Next, within our enemies update loop (step 6) we’ll also loop through the lasers table to check collisions; if so, we delete both the laser and the enemy, destroying them both. Kerpow!
Game over
The tides of battle have turned, but it’s hardly a fair fight. Let’s reuse the same collision function to check if an enemy has struck the player. Again, within the enemy update loop, we check for collision with a point in the player’s ship. If we find a collision, we’ll declare a new variable gameover = true (cue dramatic music). We will then wrap the player move and draw code in a conditional if not gameover then [code] end, so that the player can’t keep playing, and a print statement in _draw() to really hammer the point home.
They just keep coming
So now we have our pilot, lasers, aliens, and some collisions. But let’s increase the tempo and have aliens arriving in ever-increasing waves. To do this we will create a timer that increments each frame, and spawn a new wave every three seconds. Declare wavetimer = 0 and waveintensity = 5 in _init() and then, in _update(), increment the timer by one. Let’s also include a conditional that spawns a new wave, and increases the intensity, when the timer hits 90 (30 frames per second × 3).
Space debris
Now we need to do a bit of tidying up. For example, those lasers you’ve been firing? They don’t actually stop off screen, you know. They continue forever, and will eventually start slowing down PICO-8 as it tries to process thousands of off-screen lasers. The same for aliens. To fix this, within the laser and enemy update loops, check if each is out of screen bounds (0–127 for both x and y) and delete any strays. Additionally, to prevent the player from going off screen, add player.x = mid(0,player.x,120), and the same for y, in _update().
Add a high score to your PICO-8 game
Survival is one thing, but high scores are better. To cap this tutorial off, create a new variable score = 0 in _init() and add a new line when an alien is destroyed that adds to score. Choose whatever amount you want, but 100 sounds good, doesn’t it? Adding print('score: '..score,2,2,7) to the end of _draw() should show the score on screen. That’s all for now, but we’ll be looking at graphics and sound in the next few issues, as well as giving our little space-shooter some more oomph!
Click here to download the SpaceShooter code from GitHub
--attack of the green blobs --by dan lambton-howard function _init() -- called once at start player = {x=20, y=64, sprite=1} --player table enemies = {} lasers = {} create_wave(rnd(6)+5) --start game with a wave wavetimer = 0 waveintensity = 5 score = 0 end function _update() -- called 30 times per second wavetimer+=1 if not gameover then --only move the player if not gameover if btn(0) then player.x-=2 end if btn(1) then player.x+=2 end if btn(2) then player.y-=2 end if btn(3) then player.y+=2 end if btnp(4) then create_laser(player.x+5,player.y+3) end end --stop player going off screen edges player.x=mid(0,player.x,120) player.y=mid(0,player.y,120) for enemy in all(enemies) do --enemy update loop enemy.x-=enemy.speed --move enemy left for laser in all(lasers) do --check collision w.laser if enemy_collision( laser.x,laser.y,enemy) then del(enemies,enemy) del(lasers,laser) score+=100 end end --check collision w/ player if enemy_collision( player.x+4,player.y+4,enemy) then gameover = true end --delete enemy if off screen if enemy.x<-8 then del(enemies,enemy) end end for laser in all(lasers) do --laser update loop laser.x+=3 --move laser to the right if laser.x>130 then --delete laser if off screen del(lasers,laser) end end if wavetimer==90 then --every 3 seconds spawn wave create_wave(rnd(6)+waveintensity) wavetimer=0 -- reset timer waveintensity+=1 end end function _draw() --called 30 times per second cls() --clear screen if not gameover then spr(player.sprite,player.x,player.y) --draw player end for enemy in all(enemies) do --draw enemies spr(enemy.sprite,enemy.x,enemy.y) end for laser in all(lasers) do --draw lasers rect(laser.x,laser.y,laser.x+2,laser.y+1,8) end if gameover then --print game over to screen print('game over',50,64,7) end print('score: '..score,2,2,7) --show score on screen end --creates an enemy at x,y with random speed 1-2 function create_enemy(x,y) enemy={x=x,y=y,speed=rnd(1)+1,sprite=2} add(enemies,enemy) end --spawns a wave of enemies off screen function create_wave(size) for i=1,size do create_enemy(256,rnd(128)) end end function create_laser(x,y) laser = {x=x,y=y} add(lasers,laser) end --returns true if x,y are within a 8x8 rectangle around enemy function enemy_collision(x,y,enemy) if x>=enemy.x and x<=enemy.x+8 and y>=enemy.y and y<=enemy.y+8 then return true end return false end