Getting started with game development on the Raspberry Pi

How to program games with the LÖVE gaming engine on the Raspberry Pi

What's the next step after you've outgrown drag-and-drop programming? We explain how to get started with the LÖVE game engine, which uses the scripting language Lua.

How to program games with the LÖVE gaming engine on the Raspberry Pi
Image credits : 

Dwight Sipler on Flickr

The Raspberry Pi is famous for introducing kids to open source software and programming. The Pi is an affordable, practical introduction to professional-grade computing, disguised as hackable fun. An application that's done the most to get young children started in programming has been Mitch Resnick's Scratch (which fortunately was forked by the Pi Foundation when Scratch 2 switched to the non-open Adobe Air), but an inevitable question is what someone should graduate to after they've outgrown drag-and-drop programming.

After a drag-and-drop intro like Scratch, there are many candidates for the next level of programming. There's the excellent PyGame, there's a Java subset called Processing, the powerful Godot engine, and many others. The trick is to find a framework that is easy enough to ease the transition from the instant gratification of drag-and-drop, but complex enough to accurately represent what professional programmers actually do all day.

A particularly robust game engine is called LÖVE. LÖVE uses the scripting language Lua, which doesn't get as much attention as Python, but is heavily embedded (both literally and figuratively) in the modern video game industry. Lua is listed as a necessary skill by almost all the major game studios, it's an option in the proprietary Unity and Unreal game engines, and it generally pops up in all kinds of unexpected places in the real world.

To make a long story short, Lua is a language worth learning, especially if you're planning on going into game development. As far as the LÖVE engine is concerned, it's about as good as the PyGame framework in terms of features, and because it has no IDE, it is very lightweight and, in a way, less complex to learn than something like Godot.

Better yet, LÖVE runs on the Pi natively, but its projects can be open and run on any platform that Lua does. That includes Linux, Windows, and Mac, but also Android and even the oh-so-closed iOS. In other words, LÖVE is not a bad platform to get started in mobile development, too.

Mr. Rescue, an open source game available on itch.io

Mr. Rescue, an open source game available on itch.io

Now that I've sold you on LÖVE as the next step up from Scratch on the Pi, let's dig in and see how it works.

Installing LÖVE

As usual, installing LÖVE is just one easy command on the Raspberry Pi:

   $ sudo apt install love2d

If you're running Fedora on your Pi, use dnf instead:

   $ sudo dnf install love2d

Either way, the package management system pulls in Lua, SDL, and other dependencies that LÖVE needs to run.

Hello, World!

There's not much to see in the LÖVE engine. It's really a framework, meaning you can use whatever text editor you prefer. First thing to try is a "hello world" program, to make sure it launches, and to introduce you to the basic syntax of both the Lua language and the LÖVE framework. Open a text editor and enter this text:

    cwide = 520
    chigh = 333

    love.window.setTitle(' Hello Wörld ')
    love.window.setMode(cwide, chigh)

    function love.load()
	love.graphics.setBackgroundColor(177, 106, 248)
	-- font = love.graphics.setNewFont("SkirtGirl.ttf", 72)
    end

    function love.draw()
	love.graphics.setColor(255, 33, 78)
	love.graphics.print("Hello World", cwide/4, chigh/3.33)
    end

Save that as main.lua.

A distributable LÖVE package is just a standard zip file, with the .love extension. The main file, which must always be named main.lua, must be at the top level of the zip file. Create it like this:

    $ zip hello.love main.lua

And launch it:

    $ love ./hello.love

Hello World

The code is pretty easy to understand. The cwide and chigh variables are global, available to the whole script, and they set the width and height of the game world. In the first block of code (called a function), the background color is set, and in the second function the "hello world" gets printed to the screen.

The line preceded by two dashes (--) is a comment. Including a .ttf in the .love package and uncommenting the setNewFont line renders text in that font face. You don't have to use the Skirt Girl font, of course; just grab a font from FontLibrary.org and adjust the code accordingly.

    $ zip hello.love main.lua SkirtGirl.ttf

And launch it:

    $ love ./hello.love

The basics

One big difference between Scratch and a professional programming language is that with Scratch, you can learn a few basic principles and then randomly explore until you've discovered all the other features.

With lower-level programming languages like Lua, the things you learn first aren't things you can copy and paste to produce a playable game. You learn how to use the language, and then you look up functions that accomplish actions that you want your game to feature. Until you learn how the language works, stumbling your way into a working game world is still difficult.

Luckily, you can learn the language as you build the start of a game. Along the way, you pick up tricks that you can reuse later to make your game work the way you want it to work.

To begin, you need to know the three main functions in the core LÖVE engine:

  • function love.load() is the function that gets triggered when you launch a LÖVE game (it's called the init or void setup() function in other languages). It's used to set up the groundwork for your game world. function love.load() only gets executed once, so it only contains code that persists throughout your game. It's, more or less, the equivalent of the When Green Flag is Clicked block and the Stage script area in Scratch.
  • function love.update(dt) is the function that gets constantly updated during game play. Anything in a love.update(dt) function is refreshed as the game is played. If you've played any game, whether it's Five Nights at Freddy's or Skyrim or anything else, and you've monitored your frame rate (fps), everything happening as the frames tick would be in an update loop (not literally, because those games weren't made with LÖVE, but something like an update function).
  • function love.draw() is a function that causes the engine to instantiate the graphical components that you created in the love.load() function of your game. You can load a sprite or create a mountain, but if you don't draw it, then it never shows up in your game.

Those are the three functions that serve as the foundation for any element you create in LÖVE. You can create new functions of your own, too. First, let's explore the LÖVE framework.

Sprites

The basics of the "Hello, World!" is a good starting point. The same basic three functions still serve as the basic skeleton of the application. Unlike rendering simple text on the screen, though, this time we create a sprite using a graphic from openclipart.org.

The code examples and assets for this example are available in a git repository. Clone it for yourself if you want to follow along:

    git clone https://notabug.org/seth/lessons_love2d.git

Most objects in LÖVE are stored in arrays. An array is a little like a list in Scratch; it's a bunch of characteristics placed into a common container. Generally, when you create a sprite in LÖVE, you create an array to hold attributes about the sprite, and then list the attributes in love.load that you want the object to have.

In the love.draw function, the sprite gets drawn to the screen.

    fish  = {}
    cwide = 520
    chigh = 333
    
    love.window.setMode(cwide, chigh)
    love.window.setTitle(' Collide ')
    
    function love.load()
	fish.x    = 0
	fish.y    = 0
	fish.img  = love.graphics.newImage('images/fish.png')
    end
    
    function love.update(dt)
        -- this is a comment
    end
    
    function love.draw()
    	love.graphics.draw(fish.img, fish.x, fish.y, 0, 1, 1, 0, 0)
    
    end

The fish's natural predator is one of the most vicious creatures of the antarctic: the penguin. Create a penguin sprite in the same way as the fish sprite was created, with the addition of a player.speed, which is how many pixels the penguin will move once we get around to setting up player controls:

    fish  = {}
    player= {}
    cwide = 520
    chigh = 333
    
    love.window.setMode(cwide, chigh)
    love.window.setTitle(' Collide ')
    
    function love.load()
	fish.x    = 0
	fish.y    = 0
	fish.img  = love.graphics.newImage('images/fish.png')
        player.x     = 100
        player.y     = 100
        player.img   = love.graphics.newImage('images/tux.png')
        player.speed = 10
    end
    
    function love.update(dt)
        -- this is a comment
    end
    
    function love.draw()
    	love.graphics.draw(fish.img,  fish.x,  fish.y,  0,1,1,0, 0)
        love.graphics.draw(player.img,player.x,player.y,0,1,1,0, 0)
    end

To keep things tidy, place the png files in an images directory. To test the game as it is so far, zip up the main.lua and png files:

    $ zip game.zip main.lua -r images
    $ mv game.zip game.love
    $ love ./game.love

Our current cast of characters.

Our current cast of characters

Movement

There are several ways to implement movement in LÖVE, including functions for mouse, joystick, and keyboard. We've already established variables for the player's x and y positions, and for how many pixels the player moves, so use an if/then statement to detect key presses, and then redefine the variable holding the position of the player sprite using simple maths:

    player     = {}
    fish       = {}
    cwide      = 520
    chigh      = 333
    
    love.window.setMode(cwide, chigh)
    love.window.setTitle(' Collide ')

    function love.load()
	fish.x    = 0
	fish.y    = 0
	fish.img  = love.graphics.newImage( 'images/fish.png' )
	player.x     = 100
	player.y     = 100
	player.img   = love.graphics.newImage('images/tux.png')
	player.speed = 10
    end

    function love.update(dt)
	if love.keyboard.isDown("right") then
	    player.x = player.x+player.speed

	elseif love.keyboard.isDown("left") then
	    player.x = player.x-player.speed

	elseif love.keyboard.isDown("up")  then
	    player.y = player.y-player.speed    

	elseif love.keyboard.isDown("down") then
	    player.y = player.y+player.speed
        end
    end

    function love.draw()
	love.graphics.draw(player.img,player.x,player.y,0,1,1,0, 0)
	love.graphics.draw(fish.img, fish.x, fish.y, 0, 1, 1, 0, 0)    
    end

Test the code to confirm that movement works as expected. Remember to re-zip all the files before testing so that you don't accidentally test the old version of the game.

To make the movement make a little more sense, we can create functions to change the direction a sprite is facing depending on what key is pressed. This is the equivalent of these kinds of Scratch code blocks:

Looking and moving in Scratch

Looking and moving in Scratch

Generate a flipped version of the player sprite using ImageMagick:

    $ convert tux.png -flop tuxleft.png

And then write a function to swap out the image. You need two functions: one to swap out the image, and another to restore it to the original:

    function rotate_left()
	player.img = love.graphics.newImage('images/tuxleft.png')
    end

    function rotate_right()
	player.img = love.graphics.newImage('images/tux.png' )
    end

Use these functions where appropriate:

    function love.update(dt)
	if love.keyboard.isDown("right") then
	    player.x = player.x+player.speed
            rotate_right()

	elseif love.keyboard.isDown("left") then
	    player.x = player.x-player.speed
            rotate_left()
	    
	elseif love.keyboard.isDown("up")  then
	    player.y = player.y-player.speed    

	elseif love.keyboard.isDown("down") then
	    player.y = player.y+player.speed
        end
    end

Automated movement

Using the same convention of the sprite's x value, and a little mathematics, you can make the fish automatically move across the screen from edge to edge. This is the equivalent of doing this in Scratch:

Automated movement in Scratch

Automated movement in Scratch

There is no if on edge, bounce in LÖVE, but by checking whether the fish's x value has reached the far left of the screen, which is 0 pixels, or the far right, which is the same as the width of the canvas (the cwide variable), you can determine whether the sprite has gone off the edge or not.

If the fish is at the far left, then it has reached the edge of the screen, so you increment the fish's position forcing it to move right. If the fish is at the far right, then decrement the fish's position, making it move left.

    function automove(obj,x,y,ox,oy)
        if obj.x == cwide then
	    local edgeright = 0
	elseif obj.x == 0 then
	    local edgeright = 1
	end

	if edgeright == 1 then
	    obj.x = obj.x + x
	else
	    obj.x = obj.x - x
	end
    end

There's a sequence of events that happens to be important in this case. In the first if block, you check for the value of x and assign a temporary (local) variable to indicate where the fish needs to go next. Then in a second, separate if block, you move the fish. For bonus points, try doing it all in one if statement and see whether you can understand why it fails. For more bonus points, see whether you can figure out a different way of moving the fish.

To implement the fish's movement function, call the function at the bottom of the love.update loop:

    automove(fish,1,0,fish.img:getWidth(),fish.img:getHeight() )

If you test the script, you'll notice that the fish goes all the way off the screen when it hits the right edge. It's doing this because a sprite's x value is based on its upper left pixel. I'll leave it as an exercise for you to figure out what variable you should subtract from cwide when checking for the fish's position.

Collision detection

Video games are all about collisions. It's when things bump into each other, whether those things are an unfortunate hero taking a dive into a lava pit, or a bad guy getting blasted with a mana bolt, stuff is supposed to happen.

Before detecting a collision, let's decide what we want to have happen when a collision occurs. Because you already know how to change a sprite's appearance, we'll create two functions: one to change the fish into the bones left over after the penguin has caught it, and the other to change the fish back to life any time the penguin isn't around.

    function falive()
        fish.img = love.graphics.newImage('images/fish.png')
    end

    function fdead()
        fish.img = love.graphics.newImage('images/fishbones.png')
    end

With these functions set up, it's time to calculate collisions.

In Scratch, there are code blocks to check whether two sprites are touching.

Scratch collision

Scratch collision

The same concept, in principle, applies in LÖVE. There are several different ways to detect collisions, including external libraries like HC and love.physics, but a good compromise between the two is a custom function to detect an overlap in sprite boundaries.

    function CheckCollision(x1,y1,w1,h1, x2,y2,w2,h2)
        return x1 < x2+w2 and
	    x2 < x1+w1 and
	    y1 < y2+h2 and
	    y2 < y1+h1
    end

The math is complex, but it makes sense if you take a moment to think about it. The goal is detect whether two images are overlapping. If the images are overlapping, they can be said to have collided. Here's an illustration of two boxes not* overlapping, with sample y values to keep things simple:

Two boxes

Two boxes

Take a line from the function and crunch the numbers:

    y2  < y1 + h1
    110 < 0  + 100

That's obviously a false statement, so the function must return false. In other words, the boxes have not collided.

Now look at the same logic with two overlapping boxes:

Overlapping boxes

    y2  < y1 + h1
    50  < 0  + 100

This one is clearly true. Assuming all statements in the function are also true (and they would be, had I bothered adding x values), then the function returns true.

To leverage the collision check, evaluate it with an if statement. You know now that if the CheckCollision function returns true, then there is a collision. The CheckCollision function has been written generically, so when calling it, you need to feed it the appropriate values so it knows which object is which.

Most of the values are intuitive. You need LÖVE to use the x and y positions of each object being checked for collision, and the size of the object. The only values that are special in this case is the one that gets swapped out depending on the collision state. For those, hard code the size of the dead fish rather than the live fish, or else the state of the collision will get changed in the middle of the collision detection. In fact, if you want to see it glitch, you can do it the wrong way first:

    if CheckCollision(fish.x,fish.y,fish.img:getWidth(),fish.img:getHeight(), player.x,player.y,player.img:getWidth(),player.img:getHeight() ) then
        fdead()
    else
	falive()
    end

The right way is to get the size of the smaller sprite image. You can do this with ImageMagick:

    $ identify images/fishbones.png
    images/fishbones.png PNG 150x61 [...]

Then hard code the "hot spot" with the appropriate dimensions:

    if CheckCollision(fish.x,fish.y,150,61, player.x,player.y,player.img:getWidth(),player.img:getHeight() ) then
        fdead()
    else
	falive()
    end

The side effect of hard coding the collision detection area is that when the penguin touches the edge of the fish, the fish doesn't get gobbled up. For more precise collision detection, explore the HC library or love.physics.

Final code

It's not much of a game, but it demonstrates the important elements of video games:

    player     = {}
    fish       = {}
    cwide      = 520
    chigh      = 333

    love.window.setTitle(' Hello Game Wörld ')
    love.window.setMode(cwide, chigh)

    function love.load()
	fish.x    = 0
	fish.y    = 0
	fish.img  = love.graphics.newImage( 'images/fish.png' )
	player.x     = 150
	player.y     = 150
	player.img   = love.graphics.newImage('images/tux.png')
	player.speed = 10
    end

    function love.update(dt)
	if love.keyboard.isDown("right") then
	    player.x = player.x+player.speed

	elseif love.keyboard.isDown("left") then
	    player.x = player.x-player.speed

	elseif love.keyboard.isDown("up")  then
	    player.y = player.y-player.speed    

	elseif love.keyboard.isDown("down") then
	    player.y = player.y+player.speed
        end

	if CheckCollision(fish.x,fish.y,151,61, player.x,player.y,player.img:getWidth(),player.img:getHeight() ) then
	    fdead()
	else
	    falive()
	end

	automove(fish,1,0,fish.wide,fish.high)
    end

    function love.draw()
	love.graphics.draw(player.img,player.x,player.y,0,1,1,0, 0)
	love.graphics.draw(fish.img, fish.x, fish.y, 0, 1, 1, 0, 0)    
    end

    function automove(obj,x,y,ox,oy)
	if obj.x == cwide-fish.img:getWidth() then
	    edgeright = 0
	    elseif obj.x == 0 then
	    edgeright = 1
	end

	if edgeright == 1 then
	    obj.x = obj.x + x
	else
	    obj.x = obj.x - x
	end
    end

    function CheckCollision(x1,y1,w1,h1, x2,y2,w2,h2)
	return x1 < x2+w2 and
	    x2 < x1+w1 and
	    y1 < y2+h2 and
	    y2 < y1+h1
	end

    function rotate_left()
	player.img = love.graphics.newImage('images/tuxleft.png')
    end

    function rotate_right()
	player.img = love.graphics.newImage('images/tux.png' )
    end

    function falive()
	fish.img = love.graphics.newImage('images/fish.png')
    end

    function fdead()
	fish.img = love.graphics.newImage('images/fishbones.png')
    end

From here you can use the principles you've learned to create more exciting work. Collisions are the basis for most interactions in video games, whether it's to trigger a conversation with an NPC, manage combat, pick up items, set off traps, or almost anything else, so if you master that, the rest is repetition and elbow grease.

So go and make a video game! Share it with friends, play it on mobile, and, as always, keep leveling up.

1 Comments

fastbyte01

Very nice article. Thanks for sharing.

Vote up!
0
Vote down!
0

Comment now