Build a Retro game with PICO-8 for Raspberry Pi

By Lucy Hattersley. Posted

You’ve played other people’s PICO-8 games, now it’s time to make your own! Create a retro space-shooter whilst learning the Lua language

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.

 PICO-8 has a strict limit on code complexity, great for avoiding feature creep!

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.

 Those blobs came from the moon! Don’t worry too much about graphics at this stage

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!

 It’s amazing how quickly you can get a game up and running on PICO-8

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

 

 

More articles from The MagPi magazine

Subscribe