Endless Space Shooter with LÖVE on Raspberry Pi 4

Build a game starring Nyan cat, Grumpy cats and a whole lot of cake!

Written By: Cherie Tan

Dash icon
Difficulty
Medium
Steps icon
Steps
18
The Raspberry Pi isn't just a popular choice for learning about physical computing, but also game development. If you haven't already, check out the basics on how to install, set up and use LÖVE on the Raspberry Pi 4

In this guide, we'll show you how to build an endless runner/space shooter game on the Raspberry Pi 4. Instead of aliens and bullets, we'll create a talking nyan cat, grumpy cats, shooting hearts and lots of tasty treats. 

Complete this guide to create your own space shooters game. While doing so, learn about the three main callback functions in the LOVE framework, custom functions, conditional statements, for loops, and animations.

Step 1 Overview

The Raspberry Pi is a popular choice for learning about physical computing, gaming, robotics, and a whole heap of other wondrous topics. It's  a fantastic time to tinker and play around with the Raspberry Pi 4 if you're a gaming enthusiast! After all, it's now possible to run the Dreamcast emulator, REDREAM on the Raspberry Pi 4. It's also possible to build your own games. If you haven't already, check out the basics on how to install, set up and use LÖVE on the Raspberry Pi 4

In this guide, we'll show you how to build an endless runner/space shooter game on the Raspberry Pi 4. Instead of aliens and bullets, we'll create a talking nyan cat, grumpy cats, shooting hearts and lots of tasty treats. While doing so, learn about the three main callback functions in the LOVE framework, custom functions, conditional statements, for loops, and animations.

Step 2 Create sprites

Open up your pixel art tool of choice and create some sprites. Using Aseprite, we created a start/menu screen, sprites for nyan cat and a rainbow trail (player), grumpy cat (obstacles), cake (items) and a tiny heart (projectile). 
In this guide, we've created a start/menu screen (800x600 pixels), a space background image (800x600 pixels), a spritesheet for nyan cat (320x64 pixels), static images (64x64 pixels) for the cake, grumpy cat, and heart (16x16 pixels).

Step 3 Load images

function love.load()
        startscreen = love.graphics.newImage("startscreen.png")
        startbtn = love.graphics.newImage("startbutton.png")
        startblurb = love.graphics.newImage("startblurb.png")
        background = love.graphics.newImage("mainbg.png")
        cakeSprite = love.graphics.newImage("item.png")
        enemySprite = love.graphics.newImage("grumpycat.png")
end
Now that the sprites have been created, it's time to load them up into the game. The love.load function is called exactly once at the beginning of the game. This is where we load our image assets for the game.
For each sprite, load it with love.graphics.newImage and store it in a separately named variable. These variables will be used to refer to the sprite in the program.
A variable can be thought of as a word in which you can store a value. For example, using the variable enemySprite, the grumpycat.png sprite is stored to it by using the love.graphics.newImage function.

Step 4 Set custom font

function love.load()
        startscreen = love.graphics.newImage("startscreen.png")
        startbtn = love.graphics.newImage("startbutton.png")
        startblurb = love.graphics.newImage("startblurb.png")
        background = love.graphics.newImage("mainbg.png")
        cakeSprite = love.graphics.newImage("item.png")
        enemySprite = love.graphics.newImage("grumpycat.png")
        font = love.graphics.newFont("Retro Gaming.ttf", 24)
        love.graphics.setFont(font)
end
The love.load function is also where we can load any custom fonts by using love.graphics.newFont 
Remember to set the font in the game by using love.graphics.setFont

Step 5 Create variables and tables in love.load

function love.load()
        startscreen = love.graphics.newImage("startscreen.png")
        startbtn = love.graphics.newImage("startbutton.png")
        startblurb = love.graphics.newImage("startblurb.png")
        background = love.graphics.newImage("mainbg.png")
        cakeSprite = love.graphics.newImage("item.png")
        enemySprite = love.graphics.newImage("grumpycat.png")
        font = love.graphics.newFont("Retro Gaming.ttf", 24)
        love.graphics.setFont(font)
        currentScreen = 'menu'
        Sx, Sy = love.graphics.getDimensions()
        items = {}
        enemies = {}
end
You can do more than store sprites in variables. Create a new variable named currentScreen and set its value to the string menu

This string will be used to check on the game state later on in our code.
To easily get the width and height in pixels of the game window within our code, this can be done by using the love.graphics.getDimensions function, and place them in variables Sx and Sy
Next, create a table and assign it to variable items to store the cakes.
Then create another empty table and assign it to the variable enemies which will be used to store the grumpy cats.
To create a table use the table constructor, which are defined by using curly brackets, i.e. { }.

Tables are the main data structuring mechanism in Lua. We can use tables to represent lists, arrays, sets, and many other data structures.

In fact, they are the only "container" type in Lua programming. These are associative which means they store key/value pairs. In a key/value pair, you can store a value under a key and then later retrieve the value by using that particular key. 

Step 6 Anim8 library

local anim8 = require 'anim8'
local playerSpritesheet, animation 
Then place this code at the very top of the program, before love.load
How can we implement animations into the game? One quick and easy way is to incorporate a library such as anim8 which helps you create animations for LÖVE.

The latest version of anim8 can always be found on its github page, so navigate over to: https://github.com/kikito/anim8

Download the folder then copy the anim8.lua file into your project folder where main.lua is stored.

Step 7 function createPlayer()

function createPlayer()
        player = {
                x = 0,
                y = 250, 
                health = 3,
                alive = true,
                score = 0,
                bullets = {},
                bulletSprite = love.graphics.newImage("heartbullet.png"),
                rainbow = love.graphics.newImage('rainbow-trails.png'),
                cooldown = 20,
                speed = 5,
                dialogue = {},
                sentences = {"FOOD!","NYAAAAN!","Cake!","Yay.","OM NOM NOM"},
                fire = function()
                if player.cooldown <= 0 then 
                        player.cooldown = 50
                        bullet = {}
                        bullet.x = player.x + 35
                        bullet.y = player.y + 25
                        table.insert(player.bullets, bullet)
                end
        end
        }
        playerSpritesheet = love.graphics.newImage('player-spritesheet.png')
        g = anim8.newGrid(64,64,playerSpritesheet:getWidth(),playerSpritesheet:getHeight())
        g2 = anim8.newGrid(64,64,player.rainbow:getWidth(),player.rainbow:getHeight())
        -- animation for player sprite
        animation = anim8.newAnimation(g('1-4',1),0.1)
        -- animation for rainbow trail
        animation2 = anim8.newAnimation(g2('1-1',1),0.1)
end
Next, create a new function and name it createPlayer. Here, create a new table and name it player, then add various attributes of the player such as its x coordinate, y coordinate, health, score, and so on. 
So that nyan cat has multiple lines for dialogue, two tables were created. The first table was named sentences which holds five strings while the second table was empty and named dialogue. Every time a collision between nyan cat and cake sprite is detected, a random string from sentences is inserted into the dialogue table.

Step 8 Collisions

function collision(x1, y1, width1, height1, x2, y2, width2, height2)
        if x2 + width2 > x1 and x2 < x1 + width1 then
                if y2 + height2 > y1 and y2 < y1 + height1 then
                        return true
                end
        end
        return false
end
To create a collision check function, we will be checking between "rectangles" or "collision boxes". Add the following function that will detect when collisions occur. 

Step 9 function newItem()

function newItem(x,y)
        item = {}
        item.x = x
        item.y = y
        function item.update(dt)
                for i, item in pairs(items) do
                        if item.x < -10 then 
                                table.remove(items,i)
                        end
                        if collision(player.x, player.y, 64, 64, item.x, item.y, 16, 16) then 
                                table.remove(items, i)
                                player.score = player.score + 10
                                table.insert(player.dialogue,player.sentences[math.random(#player.sentences)])
                                player.speaking = true
                        end
                end
        end
        table.insert(items,item)
end
What's a game without some obstacles and items? Create a new function and name it newItem() 

Step 10 function newEnemy()

function newEnemy(x,y)
        enemy = {}
        enemy.x = x
        enemy.y = y 
        function enemy.update(dt)
                for i, enemy in pairs(enemies) do
                        if enemy.x < -10 then
                                table.remove(enemies,i)
                        end
                        for i, b in pairs(player.bullets) do 
                                if collision(bullet.x,bullet.y,16,16,enemy.x,enemy.y,64,64) then
                                        table.remove(enemies,i)
                                        table.remove(player.bullets,i)
                                        player.score = player.score + 100
                                end
                        end
                        if collision(player.x,player.y,64,64,enemy.x,enemy.y,32,32) then
                                table.remove(enemies,i)
                                if player.health > 0 then 
                                        player.hit = true 
                                        player.health = player.health - 1
                                        animation = anim8.newAnimation(g('5-5',1),0.1)
                                end
                        end
                end
        end
        table.insert(enemies,enemy)
end
Create a new function and name it newEnemy()

Step 11 function love.load()

function love.load()
        startscreen = love.graphics.newImage("startscreen.png")
        startbtn = love.graphics.newImage("startbutton.png")
        startblurb = love.graphics.newImage("startblurb.png")
        background = love.graphics.newImage("mainbg.png")
        enemySprite = love.graphics.newImage("grumpycat.png")
        cakeSprite = love.graphics.newImage("item.png")
        backgroundPosition = 0
        currentScreen = 'menu'
        font = love.graphics.newFont("Retro Gaming.ttf", 24)
        love.graphics.setFont(font)
        Sx, Sy = love.graphics.getDimensions()
        items = {}
        enemies = {}
        time = 0
        timeLimit = 3
        createPlayer()
        -- spawn items at start
        for i = 1, math.random(0,10) do
                newItem(math.random(0,700),math.random(0,500))
        end
end
Update love.load with the following code. Now, the createPlayer function will be called immediately when the game starts. Using a for loop and math.random function, a randomised number of cakes will be generated at random x and y coordinates.
We have created two very important variables : time and timeLimit 

These variables will be crucial for the generation of cakes and grumpy cats throughout the game. 

Step 12 function love.update(dt)

function love.update(dt)
        if currentScreen == 'menu' then
                menuUpdate(dt)
        elseif currentScreen == 'game' then
            gameUpdate(dt)
            animation:update(dt)
        end
end
For our menu screen, we simply want to display the start/menu screen created earlier on, until the 'x' key has been pressed. Once the 'x' key is pressed, then the game starts. So go ahead and add a conditional statement if currentScreen == 'menu' then 
Although we have not yet created the menuUpdate(dt) function, it is within love.update(dt) where it will be called. So go ahead and call menuUpdate(dt). 
Then add another condition elseif currentScreen == 'game' then 
Again, these functions have not yet been created but it is here where they will be called: gameUpdate(dt) and animation:update(dt)

Step 13 function love.draw()

function love.draw()
        if currentScreen == 'menu' then
                -- draw start menu
                love.graphics.draw(startscreen, Sx/2,Sy/2, 0, 1, 1, startscreen:getWidth()/2, startscreen:getHeight()/2)
                love.graphics.draw(startblurb, Sx/2+250,Sy/2+150, 0, 1, 1, startblurb:getWidth()/2, startblurb:getHeight()/2)
                love.graphics.draw(startbtn, Sx/2+225,Sy/2+225, 0, 1, 1, startbtn:getWidth()/2, startbtn:getHeight()/2)
        end
        if currentScreen == 'game' then
                -- draw the background
                love.graphics.draw(background, backgroundPosition, 0, 0, 1, 1)
                love.graphics.draw(background, backgroundPosition + 800, 0, 0, 1, 1)
                -- draw the player
                animation2:draw(player.rainbow,player.x-50,player.y)
                animation2:draw(player.rainbow,player.x-100,player.y)
                animation:draw(playerSpritesheet,player.x,player.y)
                -- draw bullets
                for _, b in pairs(player.bullets) do 
                        love.graphics.draw(player.bulletSprite,b.x,b.y,0,1,1)
                end
                -- draw items
                for _, item in pairs(items) do
                        love.graphics.draw(cakeSprite,item.x,item.y)
                end
                -- draw enemies
                for _,enemy in pairs(enemies) do
                        love.graphics.draw(enemySprite,enemy.x,enemy.y)
                end
                -- draw score
                love.graphics.print("Score: " .. player.score,50,30,0,1,1)
                -- draw player health
                love.graphics.print("Health: " .. player.health,50,60,0,1,1)
                -- draw player dialogue
                if player.speaking == true then
                        playerDialogue()
                end
                -- if player goes off bottom of screen then game over condition reached 
                if player.y > love.graphics.getHeight() or player.health == 0 then
                        animation = anim8.newAnimation(g('5-5',1),0.1)
                        love.graphics.print("GAME OVER. PRESS X TO RESTART.", 150,250)
                        player.alive = false
                        player.health = 0
                        if love.keyboard.isDown("x") then
                                love.load()
                        end
                end
        end
end
Add the following code in love.draw function. As it was the case for love.load, the love.draw function is a callback function and it is used to draw on the screen every frame.

Step 14 function menuUpdate(dt)

function menuUpdate(dt)
        if love.keyboard.isDown("x") then
                currentScreen = 'game'
        end
end
All the menuUpdate function does is check to see if the 'x' key has been pressed. Once it has been pressed, the game starts and the value of currentScreen is changed to 'game'

Step 15 function playerUpdate(dt)

function playerUpdate(dt)
-- all this code is required in gameUpdate(dt) function
        player.cooldown = player.cooldown - 1
        -- player's y position always moving by 2 every frame
        player.y = player.y+2
        -- player x coordinate shifts by 1 if it is less than 100 every frame
        if player.x < 100 then
                player.x = player.x+1
        end
        -- player controls: can move up or fire hearts
        if player.alive == true then
                if love.keyboard.isDown("up") then
                        player.y = player.y-player.speed
                end
                if love.keyboard.isDown("z") then 
                        player.fire()
                end
        end
        -- update bullets table
        for i, b in pairs(player.bullets) do
                if b.x < 10 then 
                        table.remove(player.bullets, i)
                end
                b.x = b.x + 10
        end
        function playerDialogue()
                if player.dialogue[1] then
                        if player.alive == true then 
                                love.graphics.print(player.dialogue[1],player.x+25,player.y-50)
                        end
                end
        end
end
Now that we've got a function that creates the player, we still need one that contains all the code that will update the player throughout the game. So go ahead and create a new function and name it playerUpdate(dt)

Step 16 function gameUpdate(dt)

function gameUpdate(dt)
        time = time + dt 

        if time >= timeLimit then
                -- generate items and enemies at random coordinates
                for i = 1, math.random(0,10) do
                        newItem(800 + math.random(0,700),math.random(0,500))
                end
                for i =1, math.random(0,3) do
                        newEnemy(800 + math.random(0,700),math.random(0,500))
                end
                -- at the end of the time timit, remove string from dialogue table
                table.remove(player.dialogue,1)
                -- remove time = 0 for no repeats
                time = 0
        end
        if time >= 1 then 
                if player.hit == true then 
                        animation = anim8.newAnimation(g('1-4',1),0.1)
                        player.hit = false
                end
        end

        -- items and enemies' x position always moving by 2 every frame
        for i, item in ipairs(items) do
                item.x = item.x - 2
        end
        for i, enemy in ipairs(enemies) do
                enemy.x = enemy.x - 2
        end

        -- Update the player
        playerUpdate()

        -- parallax background
        if backgroundPosition > -800 then
                backgroundPosition = backgroundPosition - dt * 100
        else
            backgroundPosition = 0
        end

        -- update items table
        for i, item in ipairs(items) do
                item.update(dt)
        end

        -- update enemies table
        for i, enemy in ipairs(enemies) do
                enemy.update(dt)
        end
end
While we could have added this chunk of code within love.update(dt), to organise it we have created a new function and named it gameUpdate(dt)

Previously, a new variable time was created and set to 0 and a timeLimit was created and set to 5 in love.load

These two variables will be used in a conditional statement within gameUpdate(dt) to create a timer for our game. As the comments in the code indicates, this timer is used to generate cakes and grumpy cats at random coordinates. It is also used to remove the latest added string from the dialogue table. Within tfunction is also where the playerUpdate function will be called, parallax scrolling created, and the items and enemies tables updated. 
dt stands for delta-time. It is the most common shorthand for delta-time, which is usually passed through love.update to represent the amount of time which has passed since it was last called. Although, the LOVE wiki does state: It is in seconds, but because of the speed of modern processors is usually smaller than 1, values like 0.01 are common
A frame refers to the image we see on screen, which gets updated a certain number of times a second. The frequency in which the frame is updated is known as frame rate, usually measured in frames per second (fps).

Step 17 Complete code

local anim8 = require 'anim8'
local playerSpritesheet, animation 

function love.load()
        startscreen = love.graphics.newImage("startscreen.png")
        startbtn = love.graphics.newImage("startbutton.png")
        startblurb = love.graphics.newImage("startblurb.png")
        background = love.graphics.newImage("mainbg.png")
        backgroundPosition = 0
        currentScreen = 'menu'
        font = love.graphics.newFont("Retro Gaming.ttf", 24)
        love.graphics.setFont(font)
        Sx, Sy = love.graphics.getDimensions()
        items = {}
        cakeSprite = love.graphics.newImage("item.png")
        time = 0
        timeLimit = 3
        createPlayer()
        enemies = {}
        enemySprite = love.graphics.newImage("grumpycat.png")
        -- spawn items at start
        for i = 1, math.random(0,10) do
                newItem(math.random(0,700),math.random(0,500))
        end
end

function createPlayer()
        player = {
                x = 0,
                y = 250, 
                health = 3,
                alive = true,
                score = 0,
                bullets = {},
                bulletSprite = love.graphics.newImage("heartbullet.png"),
                rainbow = love.graphics.newImage('rainbow-trails.png'),
                cooldown = 20,
                speed = 5,
                dialogue = {},
                sentences = {"FOOD!","NYAAAAN!","Cake!","Yay.","OM NOM NOM"},
                fire = function()
                if player.cooldown <= 0 then 
                        player.cooldown = 50
                        bullet = {}
                        bullet.x = player.x + 35
                        bullet.y = player.y + 25
                        table.insert(player.bullets, bullet)
                end
        end
        }
        playerSpritesheet = love.graphics.newImage('player-spritesheet.png')
        g = anim8.newGrid(64,64,playerSpritesheet:getWidth(),playerSpritesheet:getHeight())
        g2 = anim8.newGrid(64,64,player.rainbow:getWidth(),player.rainbow:getHeight())
        animation = anim8.newAnimation(g('1-4',1),0.1)
        animation2 = anim8.newAnimation(g2('1-1',1),0.1)
end

function playerUpdate(dt)
-- all this code is required in gameUpdate(dt) function
        player.cooldown = player.cooldown - 1
        -- player's y position always moving by 2 every frame
        player.y = player.y+2
        -- player x coordinate shifts by 1 if it is less than 100 every frame
        if player.x < 100 then
                player.x = player.x+1
        end
        -- player controls: can move up or fire hearts
        if player.alive == true then
                if love.keyboard.isDown("up") then
                        player.y = player.y-player.speed
                end
                if love.keyboard.isDown("z") then 
                        player.fire()
                end
        end
        -- update bullets table
        for i, b in pairs(player.bullets) do
                if b.x < 10 then 
                        table.remove(player.bullets, i)
                end
                b.x = b.x + 10
        end
        function playerDialogue()
                if player.dialogue[1] then
                        if player.alive == true then 
                                love.graphics.print(player.dialogue[1],player.x+25,player.y-50)
                        end
                end
        end
end


function collision(x1, y1, width1, height1, x2, y2, width2, height2)
        if x2 + width2 > x1 and x2 < x1 + width1 then
                if y2 + height2 > y1 and y2 < y1 + height1 then
                        return true
                end
        end
        return false
end


function newItem(x,y)
        item = {}
        item.x = x
        item.y = y
        function item.update(dt)
                for i, item in pairs(items) do
                        if item.x < -10 then 
                                table.remove(items,i)
                        end
                        if collision(player.x, player.y, 64, 64, item.x, item.y, 16, 16) then 
                                table.remove(items, i)
                                player.score = player.score + 10
                                table.insert(player.dialogue,player.sentences[math.random(#player.sentences)])
                                player.speaking = true
                        end
                end
        end
        table.insert(items,item)
end


function newEnemy(x,y)
        enemy = {}
        enemy.x = x
        enemy.y = y 
        function enemy.update(dt)
                for i, enemy in pairs(enemies) do
                        if enemy.x < -10 then
                                table.remove(enemies,i)
                        end
                        for i, b in pairs(player.bullets) do 
                                if collision(bullet.x,bullet.y,16,16,enemy.x,enemy.y,64,64) then
                                        table.remove(enemies,i)
                                        table.remove(player.bullets,i)
                                        player.score = player.score + 100
                                end
                        end
                        if collision(player.x,player.y,64,64,enemy.x,enemy.y,32,32) then
                                table.remove(enemies,i)
                                if player.health > 0 then 
                                        player.hit = true 
                                        player.health = player.health - 1
                                        animation = anim8.newAnimation(g('5-5',1),0.1)
                                end
                        end
                end
        end
        table.insert(enemies,enemy)
end


function menuUpdate(dt)
        if love.keyboard.isDown("x") then
                currentScreen = 'game'
        end
end

function gameUpdate(dt)
        time = time + dt 

        if time >= timeLimit then
                -- generate items and enemies at random coordinates
                for i = 1, math.random(0,10) do
                        newItem(800 + math.random(0,700),math.random(0,500))
                end
                for i =1, math.random(0,3) do
                        newEnemy(800 + math.random(0,700),math.random(0,500))
                end
                table.remove(player.dialogue,1)
                time = 0
        end
        if time >= 1 then 
                if player.hit == true then 
                        animation = anim8.newAnimation(g('1-4',1),0.1)
                        player.hit = false
                end
        end
        -- items and enemies' x position always moving by 2 every frame
        for i, item in ipairs(items) do
                item.x = item.x - 2
        end
        for i, enemy in ipairs(enemies) do
                enemy.x = enemy.x - 2
        end
        -- Update the player
        playerUpdate()
        -- parallax background
        if backgroundPosition > -800 then
                backgroundPosition = backgroundPosition - dt * 100
        else
            backgroundPosition = 0
        end
        -- update items table
        for i, item in ipairs(items) do
                item.update(dt)
        end
        -- update enemies table
        for i, enemy in ipairs(enemies) do
                enemy.update(dt)
        end
end

function love.update(dt)
        if currentScreen == 'menu' then
                menuUpdate(dt)
        elseif currentScreen == 'game' then
            gameUpdate(dt)
            animation:update(dt)
        end
end

function love.draw()
        if currentScreen == 'menu' then
                -- draw start menu
                love.graphics.draw(startscreen, Sx/2,Sy/2, 0, 1, 1, startscreen:getWidth()/2, startscreen:getHeight()/2)
                love.graphics.draw(startblurb, Sx/2+250,Sy/2+150, 0, 1, 1, startblurb:getWidth()/2, startblurb:getHeight()/2)
                love.graphics.draw(startbtn, Sx/2+225,Sy/2+225, 0, 1, 1, startbtn:getWidth()/2, startbtn:getHeight()/2)
        end
        if currentScreen == 'game' then
                -- draw the background
                love.graphics.draw(background, backgroundPosition, 0, 0, 1, 1)
                love.graphics.draw(background, backgroundPosition + 800, 0, 0, 1, 1)
                -- draw the player
                animation2:draw(player.rainbow,player.x-50,player.y)
                animation2:draw(player.rainbow,player.x-100,player.y)
                animation:draw(playerSpritesheet,player.x,player.y)
                -- draw bullets
                for _, b in pairs(player.bullets) do 
                        love.graphics.draw(player.bulletSprite,b.x,b.y,0,1,1)
                end
                -- draw items
                for _, item in pairs(items) do
                        love.graphics.draw(cakeSprite,item.x,item.y)
                end
                -- draw enemies
                for _,enemy in pairs(enemies) do
                        love.graphics.draw(enemySprite,enemy.x,enemy.y)
                end
                -- draw score
                love.graphics.print("Score: " .. player.score,50,30,0,1,1)
                -- draw player health
                love.graphics.print("Health: " .. player.health,50,60,0,1,1)
                -- draw player dialogue
                if player.speaking == true then
                        playerDialogue()
                end
                -- if player goes off bottom of screen then game over condition reached 
                if player.y > love.graphics.getHeight() or player.health == 0 then
                        animation = anim8.newAnimation(g('5-5',1),0.1)
                        love.graphics.print("GAME OVER. PRESS X TO RESTART.", 150,250)
                        player.alive = false
                        player.health = 0
                        if love.keyboard.isDown("x") then
                                love.load()
                        end
                end
        end
end
The complete code can be seen on the left.

Step 18 Conclusion

Once complete, drag and drop the game project folder onto love.exe to run it! 
Check out the guides over at https://www.littlebird.com.au/a/how-to/#raspberrypi to learn even more. Have fun coding!