Collision Detection Techniques
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 — that’s where it gets interesting.
This tutorial covers the collision detection techniques you’ll actually use in LÖVE2D games, from basic AABB to tile-based systems and physics libraries.
Prerequisites
This tutorial assumes you have a working LÖVE2D project. If you need to set one up first, see the Getting Started with LOVE 2D tutorial.
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 has an x, y position (top-left corner) plus w (width) and h (height). Here’s how you’d use it in a LÖVE2D game:
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 doesn’t account for rotation. If your game has rotated objects, you’ll need a different approach.
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
Here’s a complete example with a player and a collectible coin:
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.
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, tile-based collision is the standard approach. The world is divided into a grid, and you check which tiles the player overlaps with.
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 — bouncing, jointed objects, 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’s slower than custom collision code for simple games.
Using bump.lua
For simpler games that need better performance than physics, bump.lua is a popular lightweight collision library.
luarocks install bump
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 — 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.
See Also
- Getting Started with LOVE 2D — Set up your first LÖVE2D project
- Drawing and Animation in LOVE — Visual feedback for collisions
- Keyboard, Mouse, and Gamepad Input — Player movement controls