Collision Detection Techniques
Prerequisites
This tutorial assumes you have a working LÖVE2D project with a main.lua file and basic familiarity with the LOVE game loop callbacks. You should also be comfortable creating and drawing shapes with love.graphics. If you need to set up a project first, start with the Getting Started with LOVE 2D tutorial.
Collision detection techniques in LÖVE2D
Collision detection is one of those problems that starts simple and gets complicated fast. A rectangle overlaps another rectangle: easy. But rotated rectangles, fast-moving objects that tunnel through walls, and hundreds of objects checking each other every frame are where it gets interesting.
AABB collision detection
The simplest collision detection method is AABB (axis-aligned bounding box). This works with rectangles that don’t rotate. Two rectangles overlap when they overlap on both the X and Y axes simultaneously.
function checkAABB(a, b)
return a.x < b.x + b.w
and a.x + a.w > b.x
and a.y < b.y + b.h
and a.y + a.h > b.y
end
Each rectangle in the collision function is a plain Lua table with an x and y position for its top-left corner, plus w for width and h for height. The function compares the four edges independently rather than computing a single overlap value, which makes it straightforward to debug when collisions fail. Here is how you would use the function in a complete LÖVE2D game with player movement:
local player = {x = 100, y = 100, w = 32, h = 32}
local enemy = {x = 150, y = 120, w = 32, h = 32}
function love.update(dt)
-- Move player with arrow keys
local speed = 200 * dt
if love.keyboard.isDown("right") then player.x = player.x + speed end
if love.keyboard.isDown("left") then player.x = player.x - speed end
if love.keyboard.isDown("down") then player.y = player.y + speed end
if love.keyboard.isDown("up") then player.y = player.y - speed end
end
function love.draw()
-- Draw player in blue, enemy in red
love.graphics.setColor(0, 0, 1)
love.graphics.rectangle("fill", player.x, player.y, player.w, player.h)
love.graphics.setColor(1, 0, 0)
love.graphics.rectangle("fill", enemy.x, enemy.y, enemy.w, enemy.h)
-- Draw collision debug
if checkAABB(player, enemy) then
love.graphics.setColor(0, 1, 0)
love.graphics.print("COLLISION!", 10, 10)
end
end
The main limitation of AABB is that it does not account for rotation. If your game has rotated objects, you will need a different approach like separating axis theorem or the physics module covered later in this tutorial. For most 2D games, however, axis-aligned boxes cover the vast majority of collision needs and run fast enough to check hundreds of objects per frame.
Circle-circle collision
Circle collision is often simpler and faster than rectangle collision. Two circles collide when the distance between their centers is less than the sum of their radii.
function checkCircleCollision(c1, c2)
local dx = c1.x - c2.x
local dy = c1.y - c2.y
local dist = math.sqrt(dx * dx + dy * dy)
return dist < (c1.radius + c2.radius)
end
The function uses the Pythagorean theorem to compute the straight-line distance between two circle centres. Avoiding math.sqrt by comparing squared distances is a common optimisation, but the version above is fine for most games with fewer than a few hundred objects. Here is a complete working example that combines circle collision with a basic coin collection mechanic:
local player = {x = 400, y = 300, radius = 16}
local coins = {
{x = 100, y = 100, radius = 10, collected = false},
{x = 700, y = 500, radius = 10, collected = false},
{x = 400, y = 100, radius = 10, collected = false},
}
local score = 0
function love.update(dt)
local speed = 200 * dt
if love.keyboard.isDown("right") then player.x = player.x + speed end
if love.keyboard.isDown("left") then player.x = player.x - speed end
if love.keyboard.isDown("down") then player.y = player.y + speed end
if love.keyboard.isDown("up") then player.y = player.y - speed end
-- Check coin collection
for _, coin in ipairs(coins) do
if not coin.collected and checkCircleCollision(player, coin) then
coin.collected = true
score = score + 1
end
end
end
function love.draw()
love.graphics.setColor(1, 1, 1)
love.graphics.print("Score: " .. score, 10, 10)
-- Draw coins
for _, coin in ipairs(coins) do
if not coin.collected then
love.graphics.setColor(1, 0.84, 0)
love.graphics.circle("fill", coin.x, coin.y, coin.radius)
end
end
-- Draw player
love.graphics.setColor(0, 0, 1)
love.graphics.circle("fill", player.x, player.y, player.radius)
end
Circle collision works well for character controllers, collectibles, and any game objects that are naturally circular. It also has the nice property of being rotation-invariant: it does not matter which way a circular object is facing when you check for overlap. For mixed shapes, the next section covers combining circles with rectangles.
Circle-rectangle collision
Sometimes you need to check collision between a circle and a rectangle. This comes up when you have a circular player hitting axis-aligned platforms or obstacles.
The algorithm finds the closest point on the rectangle to the circle’s center, then checks if that point is within the circle’s radius:
function checkCircleRectCollision(circle, rect)
local closestX = math.max(rect.x, math.min(circle.x, rect.x + rect.w))
local closestY = math.max(rect.y, math.min(circle.y, rect.y + rect.h))
local dx = circle.x - closestX
local dy = circle.y - closestY
return (dx * dx + dy * dy) < (circle.radius * circle.radius)
end
Tile-based collision
For platformers and top-down grid games, tile-based collision is the standard approach. The world is divided into a grid of uniform cells, and you check which tiles the player overlaps with during each frame. This method is predictable, efficient, and easy to debug because every solid surface aligns to the same grid.
local tileSize = 32
local player = {x = 64, y = 64, w = 28, h = 28, vx = 0, vy = 0}
-- 1 = solid tile, 0 = empty
local level = {
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 1, 1, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
}
function isSolid(tileX, tileY)
if tileY < 1 or tileY > #level then return false end
if tileX < 1 or tileX > #level[1] then return false end
return level[tileY][tileX] == 1
end
function checkTileCollision(obj)
-- Get the tiles the object overlaps
local left = math.floor(obj.x / tileSize) + 1
local right = math.floor((obj.x + obj.w - 1) / tileSize) + 1
local top = math.floor(obj.y / tileSize) + 1
local bottom = math.floor((obj.y + obj.h - 1) / tileSize) + 1
for ty = top, bottom do
for tx = left, right do
if isSolid(tx, ty) then
return true, tx, ty
end
end
end
return false
end
function love.update(dt)
-- Simple gravity and movement
player.vy = player.vy + 800 * dt -- gravity
player.vx = player.vx * 0.9 -- friction
if love.keyboard.isDown("left") then player.vx = player.vx - 400 * dt end
if love.keyboard.isDown("right") then player.vx = player.vx + 400 * dt end
if love.keyboard.isDown("up") then player.vy = -300 end
player.x = player.x + player.vx * dt
player.y = player.y + player.vy * dt
-- Check collision and stop at walls
local hit = checkTileCollision(player)
if hit then
player.vy = 0
player.vx = 0
end
end
Tile-based collision is predictable, efficient, and easy to debug. It’s the right choice for platformers and top-down grid-based games.
Using LÖVE2D’s physics module
For games that need realistic physics like bouncing, jointed objects, and forces, LÖVE2D includes a built-in physics module powered by Box2D.
local world
function love.load()
love.physics.setMeter(64) -- 64 pixels = 1 meter
world = love.physics.newWorld(0, 9.81 * 64) -- gravity
-- Create a static ground
local groundBody = love.physics.newBody(world, 0, 580, "static")
local groundShape = love.physics.newRectangleShape(800, 40)
love.physics.newFixture(groundBody, groundShape)
-- Create a dynamic player
player = {}
player.body = love.physics.newBody(world, 400, 100, "dynamic")
player.shape = love.physics.newRectangleShape(0, 0, 32, 32)
player.fixture = love.physics.newFixture(player.body, player.shape)
player.body:setUserData({onCollide = false})
-- Collision callbacks
world:setCallbacks(
function(f1, f2, contact)
local data1 = f1:getBody():getUserData()
local data2 = f2:getBody():getUserData()
if data1 then data1.onCollide = true end
if data2 then data2.onCollide = true end
end
)
end
function love.update(dt)
world:update(dt)
-- Reset collision state
if player.body:getUserData() then
player.body:getUserData().onCollide = false
end
-- Keyboard input
local force = 500
if love.keyboard.isDown("left") then
player.body:applyForce(-force, 0)
end
if love.keyboard.isDown("right") then
player.body:applyForce(force, 0)
end
if love.keyboard.isDown("up") then
player.body:applyLinearImpulse(0, -200)
end
end
function love.draw()
love.graphics.setColor(0.5, 0.5, 0.5)
love.graphics.rectangle("fill", 0, 580, 800, 40)
local onCollide = player.body:getUserData() and player.body:getUserData().onCollide
if onCollide then
love.graphics.setColor(0, 1, 0)
else
love.graphics.setColor(0, 0, 1)
end
love.graphics.rectangle("fill", player.body:getX() - 16, player.body:getY() - 16, 32, 32)
end
The physics module handles complex collision detection automatically, including rotation and irregular shapes. The tradeoff is overhead: it is slower than custom collision code for simple games.
Using bump.lua
For simpler games that need better performance than the full physics module, bump.lua is a popular lightweight collision library. It provides axis-aligned collision detection and resolution without the overhead of a physics simulation. Install it through LuaRocks and then require it in your project:
luarocks install bump
Once installed, you create a bump world with a configurable cell size and then add your game objects to it. The world tracks each object’s position and resolves collisions transparently when you call the move method. This means objects stop at solid barriers instead of overlapping them, and you get a list of collision events back for free.
local bump = require("bump")
local world = bump.newWorld(32) -- 32-pixel grid cell size
local player = {x = 100, y = 100, w = 32, h = 32}
local boxes = {
{x = 300, y = 200, w = 64, h = 64},
{x = 500, y = 300, w = 64, h = 64},
}
function love.load()
-- Add player to world
world:add(player, player.x, player.y, player.w, player.h)
-- Add boxes
for _, box in ipairs(boxes) do
world:add(box, box.x, box.y, box.w, box.h)
end
end
function love.update(dt)
local speed = 200 * dt
local dx, dy = 0, 0
if love.keyboard.isDown("right") then dx = dx + speed end
if love.keyboard.isDown("left") then dx = dx - speed end
if love.keyboard.isDown("down") then dy = dy + speed end
if love.keyboard.isDown("up") then dy = dy - speed end
-- Move with collision
local actualX, actualY, cols = world:move(player, player.x + dx, player.y + dy)
player.x, player.y = actualX, actualY
-- Handle collisions
for _, col in ipairs(cols) do
print("Collided with: " .. col.other.x .. ", " .. col.other.y)
end
end
function love.draw()
-- Draw boxes
love.graphics.setColor(1, 0, 0)
for _, box in ipairs(boxes) do
love.graphics.rectangle("fill", box.x, box.y, box.w, box.h)
end
-- Draw player
love.graphics.setColor(0, 0, 1)
love.graphics.rectangle("fill", player.x, player.y, player.w, player.h)
end
bump.lua handles collision resolution automatically: the player stops at walls instead of passing through them. It also supports sliding along walls and other response behaviors.
Common gotchas
Collision detection and collision response are different things. Detecting a collision tells you that objects overlap. Responding to a collision means deciding what happens: the player stops, bounces, takes damage, or collects an item.
Fast objects tunnel through walls. If an object moves more than its own width in a single frame, it can skip over thin walls entirely. Use continuous collision detection (CCD) or cap maximum velocity to fix this.
O(n²) doesn’t scale. Checking every object against every other object works for 10 objects but breaks down at 100. Use spatial partitioning with grids, quadtrees, or spatial hashes to reduce checks.
Float precision causes jitter. In tile-based games, use integer positions for grid alignment. Small float errors accumulate and cause objects to vibrate or drift.
Next steps
Now that you can detect when objects collide, the next skill to build is making those collisions feel right. Head to Drawing and Animation in LOVE to learn how to give your game objects visual feedback with sprites and frame-by-frame animation. If you are building a platformer, the Tilemaps in LOVE 2D tutorial will show you how to combine tile-based collision with efficient rendering.
See also
- Getting Started with LOVE 2D: set up your first LÖVE2D project
- Drawing and Animation in LOVE: visual feedback for collisions
- Game States in LOVE 2D: organizing game screens with state machines