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
Difficulty
Medium
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.
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.
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.
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.
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).
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.
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
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.
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.
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.
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.
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.
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.
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()
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()
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.
These variables will be crucial for the generation of cakes and grumpy cats throughout the game.
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)
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.
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)
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.
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).
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.
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!