Detecting collisions in LÖVE games
Tutorial – LÖVE
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 = love.graphics.newImage ("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 10 11 function Mine:draw () 12 love.graphics.draw (self.image, self.body:getX(), self.body:getY(), self.body:getAngle(), 1, 1, 14, 14) 13 end
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.
You can solve this by using the offset parameters you can pass to love.graphics.draw ()
. Instead of just writing line 12 like this:
love.graphics.draw (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.
love.graphics.draw (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 love.graphics.draw ()
[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" 04 05 w_width = 900 06 w_height = 600 07 08 function love.load () 09 love.window.setMode (w_width, w_height, {resizable = false}) 10 love.graphics.setBackgroundColor (0.5, 0.8, 1, 1) 11 12 terrainG = Scenery 13 terrainG:init () 14 15 terrainP = Earth 16 terrainP:init (terrainG) 17 18 mine = Mine 19 mine:init (470, 28, 0.8, 1) 20 end 21 22 function love.update (dt) 23 world:update(dt) 24 end 25 26 function love.draw () 27 love.graphics.setColor(1, 1, 1, 1); 28 mine:draw () 29 30 terrainG:draw () 31 end
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 = love.graphics.newImage ("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 11 12 function Mine:draw () 13 love.graphics.draw (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 = love.graphics.newImage ("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 24 25 function Tank:draw () 26 love.graphics.draw (self.image, self.body:getX(), self.body:getY(), 0, 1, 1, 16, 10) 27 end
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
beginContact
gets called when two objects collide. - 2
endContact
gets called when two objects stop colliding, say, when one bounces off the other and both objects stop touching each other. - 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 thepreSolve
function, you can change that to make, for example, the bodies smoosh together under certain circumstances or break into pieces Asteroids-style. - 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 06 07 function endContact (a, b, coll) 08 end 09 10 function preSolve (a, b, coll) 11 end 12 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) 03 04 if tank_hit then 05 love.event.push('quit') 06 end 07 end
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) 03 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 22 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 27 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 love.graphics.setColor(1, 1, 1, 1); 03 love.graphics.print ('Angle: ' .. angle , 10, 10, 0, 2) 04 love.graphics.print ('Force: ' .. force , 10, 40, 0, 2) 05 06 mine:draw () 07 tank:draw () 08 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.
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
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.
News
-
Fedora Asahi Remix 41 Available for Apple Silicon
If you have an Apple Silicon Mac and you're hoping to install Fedora, you're in luck because the latest release supports the M1 and M2 chips.
-
Systemd Fixes Bug While Facing New Challenger in GNU Shepherd
The systemd developers have fixed a really nasty bug amid the release of the new GNU Shepherd init system.
-
AlmaLinux 10.0 Beta Released
The AlmaLinux OS Foundation has announced the availability of AlmaLinux 10.0 Beta ("Purple Lion") for all supported devices with significant changes.
-
Gnome 47.2 Now Available
Gnome 47.2 is now available for general use but don't expect much in the way of newness, as this is all about improvements and bug fixes.
-
Latest Cinnamon Desktop Releases with a Bold New Look
Just in time for the holidays, the developer of the Cinnamon desktop has shipped a new release to help spice up your eggnog with new features and a new look.
-
Armbian 24.11 Released with Expanded Hardware Support
If you've been waiting for Armbian to support OrangePi 5 Max and Radxa ROCK 5B+, the wait is over.
-
SUSE Renames Several Products for Better Name Recognition
SUSE has been a very powerful player in the European market, but it knows it must branch out to gain serious traction. Will a name change do the trick?
-
ESET Discovers New Linux Malware
WolfsBane is an all-in-one malware that has hit the Linux operating system and includes a dropper, a launcher, and a backdoor.
-
New Linux Kernel Patch Allows Forcing a CPU Mitigation
Even when CPU mitigations can consume precious CPU cycles, it might not be a bad idea to allow users to enable them, even if your machine isn't vulnerable.
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.