Detecting collisions in LÖVE games

Tutorial – LÖVE

Article from Issue 237/2020

To create an action-packed game with LÖVE, these are a few last things you should learn how to do – overlay fancy images to "physical" objects, detect collisions, and get input from the keyboard or mouse.

LÖVE [1] is a Lua [2] framework that lets you develop fun 2D games relatively easily. We started talking about LÖVE and how to animate sprites in Linux Magazine, issue 234 [3], and went on to examine how to use the 2D physics engine to make things fall and bounce in issue 235 [4].

In this installment, you will see how to overlay PNG images to physical objects, check when they collide, and get input from the players. With these three things, you will be in a great place to start making your own games.

Let's get started!

Physical Overlay

In the prior article [4], you saw how you can use geometric figures like rectangles, circles, and polygons as bodies you can throw around or bounce off of. But you will reach a point where you will want to use something more visually appealing than just flat objects.

As you will remember from the article in the first installment of this series [3], a LÖVE game has three essential parts: load, update, and draw. The trick is that when you get to the draw bit you use a PNG image and overlay it on the physical object (which remains invisible) and use the physical object's position and rotation on the PNG.

For our example, let's use the mine image shown in Figure 1 and a circle as the physical object for the underlying body. Listing 1 shows what the object would look like written in LÖVE.

Listing 1

pobject.lua (1)

01 Mine = {}
02 function Mine:init (posx, posy, bounciness, friction)
03   self.image = ("Sprites/mine.png")
04   self.body = love.physics.newBody (world, posx, posy, 'dynamic')
05   self.shape = love.physics.newCircleShape (14)
06   self.fixture = love.physics.newFixture (self.body, self.shape, 1)
07   self.fixture:setRestitution (bounciness)
08   self.fixture:setFriction (friction)
09 end
11 function Mine:draw ()
12 (self.image, self.body:getX(), self.body:getY(), self.body:getAngle(), 1, 1, 14, 14)
13 end
Figure 1: A PNG image of a mine you can overlay on a circular physical object.

You create a Lua table object on line 1 and then define the object itself in the init () function (lines 2 to 9). There are two differences with how you defined objects in the examples in the article in issue 235 [4]: The first is that you add a new attribute, self.image. This contains the path and name of the PNG image you will overlay on the physical object (line 3). The other is that instead of a rectangle, this time the physical object is a circle with the same radius as the PNG image of the mine, 14 pixels (line 5).

Mine's draw () function is also different. You are not drawing a circle, but using the circle's position and rotation to establish the PNG image's position and rotation in the play area. LÖVE's body:getX () and body:getY () functions supply the circle's position. The body:getAngle () function provides its rotation. Applying these values to the image's draw function, you can place your PNG mine in space and have it spin like a physical object.

There is one thing to look out for though: The physical circle rotates around its center, while the image of the mine rotates around its own origin of coordinates, which, by default, is located in the upper left-hand corner of the image. If you don't take that into account, the mine's rotation will be off-kilter. In Figure 2, you can see the image of the mine mid-rotation and the physical object (the white circle) and how they are not in sync.

Figure 2: The different points around which the image and the physical body (the white circle) rotate prevent them from lining up.

You can solve this by using the offset parameters you can pass to (). Instead of just writing line 12 like this: (self.image, self.body:getX(), self.body:getY(), self.body:getAngle ())

you have to include the offset parameters and place the origin of coordinates of the image at position 14, 14 so it coincides with the center of the physical circle, as shown below. (self.image, self.body:getX(), self.body:getY(), self.body:getAngle (), 1, 1, 14, 14)

Note that Lua does not allow for named parameters, so you also have to include the scale parameters (1, 1) that go before the offset parameters (14, 14) and that () [5] requires.

The load function on lines 8 to 20 in Listing 2 is very similar to what you saw in Linux Magazine, issue 235: You create the object on line 18 and initialize it on line 19. In this case, you will be dropping your mine from position 470, 28. As the world gets updated on line 23, the mine will fall and bounce around until it comes to a rest or bounces outside the screen (Figure 3).

Listing 2

main.lua (1)

01 require "scenery"
02 require "pworld"
03 require "pobject"
05 w_width = 900
06 w_height = 600
08 function love.load ()
09   love.window.setMode (w_width, w_height, {resizable = false})
10 (0.5, 0.8, 1, 1)
12   terrainG = Scenery
13   terrainG:init ()
15   terrainP = Earth
16   terrainP:init (terrainG)
18   mine = Mine
19   mine:init (470, 28, 0.8, 1)
20 end
22 function love.update (dt)
23   world:update(dt)
24 end
26 function love.draw ()
27, 1, 1, 1);
28   mine:draw ()
30   terrainG:draw ()
31 end
Figure 3: The mine falls, spins, and bounces around thanks to the data retrieved from the invisible physical object.

Since you saw how to create the ground and world in issue 235 [4], I won't repeat that here. You can see the complete code and not just the fragments shown here in the program's repository [6].

Colliding Objects

Once you have your mine bouncing around, you will want it to collide with something and destroy it. I drew a tank (Figure 4) and made it a body (lines 16 to 23 in Listing 3).

Listing 3

pobject.lua (2)

01 Mine = {}
02 function Mine:init (posx, posy, bounciness, friction)
03   self.image = ("Sprites/mine.png")
04   self.body = love.physics.newBody (world, posx, posy, 'dynamic')
05   self.shape = love.physics.newCircleShape (14)
06   self.fixture = love.physics.newFixture (self.body, self.shape, 1)
07   self.fixture:setRestitution (bounciness)
08   self.fixture:setFriction (friction)
09   self.fixture:setUserData ("Mine")
10 end
12 function Mine:draw ()
13 (self.image, self.body:getX(), self.body:getY(), self.body:getAngle(), 1, 1, 14, 14)
14 end
15 ---
16 Tank = {}
17 function Tank:init (posx, posy)
18   self.image = ("Sprites/tank.png")
19   self.body = love.physics.newBody (world, posx, posy, 'dynamic')
20   self.shape = love.physics.newRectangleShape (32, 19)
21   self.fixture = love.physics.newFixture (self.body, self.shape, 1)
22   self.fixture:setUserData ("Tank")
23 end
25 function Tank:draw ()
26 (self.image, self.body:getX(), self.body:getY(), 0, 1, 1, 16, 10)
27 end
Figure 4: A tank we are going to drop a mine on.

In this example, the mine will drop from the sky, bounce, and – hopefully – hit the tank. If that happens, the program will exit. If not, well… you'll just have to exit the program yourself.

To detect if one object has collided with another, you may be tempted to go old school and look at the position of the bounding boxes (the invisible boxes surrounding each object) and see if they are intersecting.

Don't do that.

LÖVE has Contact objects [7], which are objects that pop into existence when two bodies start to collide. You can use Contact objects to find out if objects are touching, to set the friction between colliding objects, to see how they will bounce off each other, and so on.

But, even better, you don't have to worry about any of that, at least not for this experiment. LÖVE provides another layer to make things simpler in the shape of callback functions that will trigger when a collision occurs.

There are four callback functions used for collisions:

  1. 1 beginContact gets called when two objects collide.
  2. 2 endContact gets called when two objects stop colliding, say, when one bounces off the other and both objects stop touching each other.
  3. 3 preSolve is called just after a collision is detected but before the programmed action that executes automatically after the collision runs. In general, the default action is that, when one body hits another, they bounce off each other. You don't have to program this explicitly; it's just what LÖVE's physics engine does. But in the body of the preSolve function, you can change that to make, for example, the bodies smoosh together under certain circumstances or break into pieces Asteroids-style.
  4. 4 postSolve is called after the collision and is usually used to collect data of the collision's effect, like what direction each object is now headed and at what speed.

Setting up the callbacks is a simple task, just add

world:setCallbacks (beginContact, endContact, preSolve, postSolve)

to your love.load () function. While you are at it also add

tank_hit = false

to the function. You will use the tank_hit variable later to record when the tank gets hit by the mine if they both collide.

Now add what you see in Listing 4 to the end of main.lua.

Listing 4

Collision Callbacks (Part of main.lua)

01 function beginContact (a, b, coll)
02   if (a:getUserData () == "Tank" or b:getUserData () == "Tank") and (a:getUserData () == "Mine" or b:getUserData () == "Mine") then
03     tank_hit = true
04   end
05 end
07 function endContact (a, b, coll)
08 end
10 function preSolve (a, b, coll)
11 end
13 function postSolve (a, b, coll, normalimpulse, tangentimpulse)
14 end

As you can see, you are only going to worry about when two objects collide – to be precise, whether the tank and mine collide. The thing is, function beginContact () triggers when any two objects collide. The tank is colliding with the ground all the time. The mine collides with the ground when it bounces. This could get confusing if you act on every time any object collides with any other object.

The a and b parameters in the function's parameter list contain the fixtures of the bodies that are colliding. Remember that a body's fixture [8] contains things like its shape, mass, degree of bounciness, and so on. You can also define your own attribute. For that you use the Fixture:setUserData () function. On lines 9 and 22 in Listing 3, you are giving each fixture a short name, "Mine" and "Tank", which you can then use on line 2 of Listing 4. The if statement will determine whether the objects colliding are "Mine" and "Tank" and, if they are, will set tank_hit to true.

You can then use that information in your update function to do something (line 5, Listing 5). In this case, if the mine hits the tank, the program exits immediately (Figure 5).

Listing 5

love.update (Part of main.lua)

01 function love.update (dt)
02   world:update(dt)
04   if tank_hit then
05     love.event.push('quit')
06   end
07 end
Figure 5: Will the mine hit the tank?

User Input

It may be fun to watch the mine land on the tank, but games are meant to be interactive! At some point, you are going to have to grab the input from the player and use it to affect the outcome of the game. Let's do that now, and, while we're at it, let's turn our sample code into a proper game (sort of) with a goal.

Using the elements you already have (a mine, a tank, some physics, and collision detection), let's make a game where the player must launch the mine from the left of the screen, over the mountain, in hopes of hitting the tank.

Start by defining four new variables in your love.load () function: aiming = true, fire = false, angle = 0, and force = 0. You will use the aiming variable in your love.update function to read in key presses that aim your mine. The fire variable tells your program when the fire button (the space bar – see below) is pressed, which is the moment to launch the mine. The angle variable is the angle at which you will launch your mine, and force is the variable that sets the strength at which you will launch it.

Now take a look at Listing 6, which shows the love.update function.

Listing 6

update (Part of main.lua)

01 function love.update (dt)
02   world:update(dt)
04   if aiming then
05     if love.keyboard.isDown("right") and mine.body:getX () < 286 then
06       mine.body:setX (mine.body:getX () + 1)
07     elseif love.keyboard.isDown("left") and mine.body:getX () > 15 then
08       mine.body:setX (mine.body:getX () - 1)
09     elseif love.keyboard.isDown("up") and angle < 90 then
10       angle = angle + 1
11     elseif love.keyboard.isDown("down") and angle > 0 then
12       angle = angle - 1
13     elseif love.keyboard.isDown ("+") and force < 1000 then
14       force = force + 1
15     elseif love.keyboard.isDown("-") and force > 0 then
16       force = force - 1
17     elseif love.keyboard.isDown("space") then
18       aiming = false
19       fire = true
20     end
21   end
23   if fire then
24     mine.body:applyLinearImpulse (math.cos (math.rad (angle)) * force, math.sin (math.rad (angle)) * force)
25     fire = false
26   end
28   if tank_hit then
29     love.event.push('quit')
30   end
31 end

The love.keyboard.isDown () function checks that the key you pass as a parameter is pressed. You use the left and right arrow keys to move the mine left and right, the up and down keys to change the angle (from 0 degrees, straight ahead, to 90 degrees, straight up). The + and - keys increase or decrease the force from between 0 and 1,000, and you use the space bar to fire.

Once the player presses the space bar, the aiming phase ends (you set the aiming variable to false), and the fire phase starts (you set the fire variable to true).

The fire phase (lines 23 to 26) is very simple: You calculate the horizontal and vertical components of the force using basic trigonometry and apply it to the mine. You may reasonably assume that the LÖVE function you need to move the mine is body:applyForce () [9], but this is more appropriate when you want to apply a force over several game cycles, like when you are accelerating a car or firing the boosters on a rocket. The thing you need here is body:applyLinearImpulse () [10], which applies a force for an instant and then lets go.

As with body:applyForce (), body:applyLinear Impulse () takes the horizontal and vertical component of a force to accelerate the body in a certain direction. You can also add where on the body you want to apply the force, thus giving it a spin, but you don't need it here.

Making a few modifications to your draw function (Listing 7), you can show your player what angle they will fire at and the force.

Listing 7

draw (Part of main.lua)

01 function love.draw ()
02, 1, 1, 1);
03 ('Angle: ' .. angle , 10, 10, 0, 2)
04 ('Force: ' .. force , 10, 40, 0, 2)
06   mine:draw ()
07   tank:draw ()
09   terrainG:draw ()
10 end

As a side note, you have to know that there are many more ways of getting a player's input. LÖVE supports key presses, mouse movements and clicks, joysticks, gamepads, and touch screens [11].

The final game looks like what you can see in Figure 6.

Figure 6: A proper game: Try to hit the tank!

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Gravity

    Video game animation is not simply a matter of making your characters move – you also have to consider the physics of the world in which they move.

  • I <3 Animation

    LÖVE is an extension of the Lua language, designed to make developing games easy. In this tutorial, we'll explore this framework by creating some animated sprites.

  • Tutorials – OpenSCAD

    OpenSCAD lets you use simple scripts to build 3D bodies from primitive shapes that you can then send to your 3D printer. It also lets you create custom shapes for pieces and objects. In this article, we look at two ways to do just that.

  • True Love … and Microsoft Love
  • Perl: OpenOffice Label Merge

    OpenOffice offers a selection of preconfigured formats for users who need to print their own self-adhesive labels. Perl feeds the address data to the document.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More