Collider = require "hardoncollider"
The main module.
HardonCollider will automatically handle - but not resolve - collisions. It uses search data structure - a spatial hash - to quickly find colliding shapes.
A spatial hash is a grid that is laid over the whole scene in which a shape can occupy
several cells. To find shapes that may be colliding, you simply need to look which shapes occupy
the same cell. You can specify the cell size in the new()
function.
To get a less boring explanation on how to use this, see the tutorial.
Initializes the library. Call this in love.load()
. All the parameters can be omitted.
Note: The cell size does not determine the granularity of the collision detection, but is an optimization device. Values that are too small or too big will have a negative impact on the detection speed. The meaning of too small and too big depends on the size of the shapes in the collision detection.
cell_size
(100)callback_collide
(empty function)callback_stop
(empty function)Collider = require 'hardoncollider'
function love.load()
HC = Collider.new(150)
-- or: HC = Collider(150)
end
Sets the collision callbacks. The second calling style let's you specify the callbacks by name, see the example.
If nil
is passed as any callback, the callback will not be changed.
The callbacks must have the following function prototype:
function callback(dt, shape_one, shape_two, mtv_x, mtv_y)
shape_one
and shape_two
are the colliding shapes. mtv_x
and mtv_y
define the minimum translation vector, i.e. the direction and magnitude shape_one has to be moved so that
the collision will be resolved. Note that if one of the shapes is a point shape, the
translation vector will be invalid.
collide
stop
Collider = require 'hardoncollider'
function collide(dt, shape_one, shape_two, mtv_x, mtv_y)
print('colliding:', shape_one, shape_two)
print('mtv:', mtv_x, mtv_y)
-- move both shape_one and shape_two to resolve the collision
shape_one:move(mtv_x/2, mtv_y/2)
shape_two:move(-mtv_x/2, -mtv_y/2)
end
function colliding_two(dt, shape_one, shape_two, mtv_x, mtv_y)
print('colliding:', shape_one, shape_two)
-- move only shape_one to resolve the collision
shape_one:move(mtv_x, mtv_y)
end
-- ignore the translation vector
function stop(dt, shape_one, shape_two)
print('collision resolved')
end
function love.load()
HC = Collider()
-- set initial callbacks
HC:setCallbacks(collide, stop)
-- change collide callback
HC:setCallbacks{collide = collide_two}
end
Checks for collisions and call callbacks. Use this in love.update(dt)
.
Note that the delta time has no effect on the collision detection itself, but will be passed to the callback functions.
dt
function love.update(dt)
HC:update(dt)
end
Add a polygon to the collision detection system. Any non-intersection polygon will work, even convex polygons.
Note that if three consecutive points lie on a line, the middle point will be discarded. This means you cannot construct polygon shapes out of lines.
x1,y1, ..., xn,yn
shape = HC:addPolygon(10,10, 40,50, 70,10, 40,30)
Add a rectangle shape to the collision detection system.
Note: Shape transformations, e.g. shape:moveTo()
and shape:rotate()
, will be with respect to the rectangle center, not the upper left corner.
x, y
w, h
rect = HC:addRectangle(100,120, 200,40)
Add a circle shape to the collision detection system.
cx, cy
radius
circle = HC:addCircle(400,300, 100)
Add a point shape to the collision detection system.
Point shapes are most useful for bullets or suchs, because detecting collisions between a point and any other shape is a little faster than detecting collision between two non-point shapes. In case of a collision, the callback will not receive a valid minimum translation vector.
x, y
bullets[#bulltes+1] = HC:addPoint(player.pos.x,player.pos.y)
Add a custom shape to the collision detection system.
The shape must implement two functions for this to work:
function shape:bbox()
return corners-of-axis-aligned-bounding-box
end
and
function shape:collidesWith(other)
local colliding = ...
local sx,sy = separating-vector(self, other)
return colliding, sx,sy
end
The shape will be augmented with the function shape:neighbors()
shape
AABB = {x = ..., y = ..., width = ..., height = ...}
function AABB:bbox()
return self.x, self.y, self.x+self.width, self.y+self.height
end
function AABB:collidesWith(other)
return magic
end
AABB = HC:addShape(AABB)
Remove a shape from the collision detection system. Note that if you remove a shape in
the collide()
callback, other shapes might still have collided with it, so the
shape will be argument to the other calls of collide()
.
In any case, the stop()
callback will be called in the next call to
update
for each shape which the removed shape collided with.
shape, ...
HC:remove(circle)
HC:remove(enemy1, enemy2)
Add or remove shapes to/from a group. Shapes in the same group will not emit collision callbacks when colliding with each other.
group
shape, ...
HC:addToGroup("platforms", platform1, platform2, platform3)
HC:removeFromGroup("platforms", platform1)
Flags a shape active or passive. Only active shapes will search for colliding shapes, i.e. there will be no collision reported when two passive shapes collide.
This enables you to significantly speed up the collision detection. Typical candidates for passive shapes are those which are numerous, but don't act in themselves, e.g. the level geometry.
shape, ...
HC:setPassive(platform1, platform2, platform3)
Sets shape to be passive. Passive shapes will be subject to collision detection, but will not actively search for collision candidates. This means that if two passive shapes collide, no collision callback will be invoked (in fact, the collision won't even be detected).
This function exists purely for performance optimisation. Use it wisely. Good candidates for passive shapes are traps and terrain.
Note: Shapes are active by default
shape, ...
hardoncollider.setPassive(ground, bridge, spikes)
Makes shapes active again.
Note: Shapes are active by default
shape, ...
hardoncollider.setActive(collapsing_bridge)
Iterator over all active shapes. Mostly for internal use.
-- rotate all active shapes
for shape in HC:activeShapes() do
shape:rotate(dt)
end
Makes a shape permeable or solid. Ghost shapes will not collide with any other shape.
shape, ...
-- make player invincible for 5 seconds
HC:setGhost(player)
Timer.add(5, function() HC:setSolid(player) end)
Retrieve a list of shapes covering the point (x,y)
,
i.e. all shapes that contain (x,y)
. This includes active,
passive, solid and ghost shapes.
x, y
(x,y)
.-- select the units under the mouse cursor
function love.mousereleased(x,y,btn)
for _, shape in ipairs(HC:shapesAt(x,y)) do
shape.object:select()
end
end
Returns all shapes contained in the rectangle (x1,y1)-(x2,y2)
.
x1, y1
x2, y2
t[shape] = shape
) of shapes.-- draw only visible shapes
local width = love.graphics.getWidth()
local height = love.graphics.getHeight()
for shape in pairs(HC:shapesInRange(0,0, width,height) do
shape.object:draw()
end
shapes = require "hardoncollider.shapes"
Shape classes with collision detection methods.
This defines methods to move, rotate and draw shapes created with hardoncollider.add*
.
As each shape is at it's core a Lua table, you can attach values and add functions
to it. Be careful though not to use keys that name a function or start with an underscore,
e.g. move
or _groups
, since these are used internally. Everything
else is fine.
If you don't want to use the full blown module, you can still use these classes to test for colliding shapes. They might also be useful for doing GUI stuff, e.g. when testing if the mouse hovers a button.
Construct a shape using a non-intersecting ploygon.
You can either specify the coordinates as with hardoncollider.addPolygon()
or use an instance of the Polygon
class.
x1,y1, ..., xn,yn
polygon
shape = shapes.newPolygonShape(100,100, 200,200, 300,100)
Construct a circular shape.
cx, cy
radius
shape = shapes.newCircleShape(400,300, 100)
Construct a point shape.
x, y
shape = shapes.newPointShape(400,300)
Test if the shape contains a given point.
x, y
true
if x,y
lies in the interior of the shape.if unit.shape:contains(love.mouse.getPosition) then
unit:setHovered(true)
end
Test if the shape intersects a ray.
x, y
dx, dy
intersection
true
if the given ray intersects the shape.t
(only if intersecting)local intersecting, t = player:intersectsRay(x,y, dx,dy)
if intersecting then -- laser pointer hits player
local vx,vy = vector.mul(t, x+dx, y+dy)
player:addMark(vx,vy)
end
Move the shape.
x, y
circle:move(10,15) -- move the circle 10 units right and 15 units down
Set the shape's position.
Important: This function moves the shape's center to (x,y)
. It is equivalent to:
local cx,cy = shape:center()
shape:move(x-cx, y-cy)
x, y
circle:moveTo(400,300) -- move circle to screen center
Scale the shape relative to it's center.
s
> 0
.circle:scale(2) -- double the circle's size
Rotate the shape. A rotation center can be specified. If no center is given, the shape's center is used.
angle
cx, cy
(optional)rectangle:rotate(math.pi/4)
Set the rotation of a shape. A rotation center can be specified. If no center is given, the shape's center is used.
Equivalent to:
shape:rotate(angle - shape.rotation, cx,cy)
angle
cx, cy
(optional)rectangle:setRotation(math.pi, 100,100)
Get the center of the shape.
If the shape is a CircleShape, returns the circle center. In case of a point shape, returns the position. Else returns the polygon's centroid.
x, y
print("Circle at:", circle:center())
Get the shape's rotation angle in radians.
angle
print("Box rotation:", box:rotation())
Get circumscribing circle.
x, y
r
if player:hasShield() then
-- draw shield
love.graphics.circle('line', player:outcircle())
end
Get axis aligned bounding box.
x1, y1
x2, y2
-- draw bounding box
local x1,y1, x2,y2 = shape:bbox()
love.graphics.rectangle('line', x1,y1, x2-x1,y2-y1)
Draw the shape either filled or as outline.
mode
'line'
or 'fill'
.circle:draw('fill')
Get furthest vertex of the shape with respect to the direction dx, dy
.
Used in the collision detection algorithm, but may be useful for other things - e.g. lighting - too.
dx, dy
dx, dy
-- get vertices that produce a shadow volume
local x1,y1 = circle:support(lx, ly)
local x2,y2 = circle:support(-lx, -ly)
Test if two shapes collide.
other
collide
true
if the two shapes collide, false
otherwise.dx, dy
nil
if the two shapes don't collide.if circle:collidesWith(rectangle) then
print("collision detected!")
end
Only available in shapes created with main module (i.e. HC:addRectangle(), …).
Iterator over neighboring shapes.
-- check for collisions with neighboring shapes
for other in shape:neighbors() do
if shape:collidesWith(other) then
print("collision detected!")
end
end
polygon = require "hardoncollider.polygon"
Definition of a Polygon class and implementation of some handy algorithms.
On it's own, this class does not offer any collision detection. If you want that, use a PolygonShape instead.
Syntax depends on used class system. Shown syntax works for bundled hump.class and slither.
Construct a polygon.
At least three points that are not collinear (being on a straight line) are needed to construct the polygon. If there are collinear points, these points will be removed so that the overall shape of the polygon is not changed.
x1,y1, ..., xn,yn
Polygon = require 'hardoncollider.polygon'
poly = Polygon(10,10, 40,50, 70,10, 40,30)
Get the polygon's vertices. Useful for drawing with love.graphics.polygon()
.
x1,y1, ..., xn,yn
love.graphics.draw('line', poly:unpack())
Get a copy of the polygon.
Since Lua uses references when simply assigning an existing polygon to a variable, unexpected things can happen when operating on the variable. Consider this code:
p1 = Polygon(10,10, 40,50, 70,10, 40,30)
p2 = p1
p3 = p1:clone()
p2:rotate(math.pi) -- p1 will be rotated, too!
p3:rotate(-math.pi) -- only p3 will be rotated
polygon
copy = poly:clone()
copy:move(10,20)
Get axis aligned bounding box.
x1, y1
x2, y2
x1,y1,x2,y2 = poly:getBBox()
-- draw bounding box
love.graphics.rectangle('line', x1,y2, x2-x1, y2-y1)
Test if a polygon is convex, i.e. a line line between any two points inside the polygon will lie in the interior of the polygon.
convex
true
if the polygon is convex, false
otherwise.-- split into convex sub polygons
if not poly:isConvex() then
list = poly:splitConvex()
else
list = {poly:clone()}
end
Move a polygon in a direction..
x, y
poly:move(10,-5) -- move 10 units right and 5 units up
Rotate the polygon. You can define a rotation center. If it is omitted, the polygon will be rotated around it's centroid.
angle
cx, cy
p1:rotate(math.pi/2) -- rotate p1 by 90° around it's center
p2:rotate(math.pi/4, 100,100) -- rotate p2 by 45° around the point 100,100
Split the polygon into triangles.
triangles
triangles = poly:triangulate()
for i,triangle in ipairs(triangles) do
triangles.move(math.random(5,10), math.random(5,10))
end
Split the polygon into convex sub polygons.
convex_polygons
convex = concave_polygon:splitConvex()
function love.draw()
for i,poly in ipairs(convex) do
love.graphics.polygon('fill', poly:unpack())
end
end
Create a merged polygon of two polygons if, and only if the two polygons share one complete edge. If the polygons share more than one edge, the result may be erroneous.
This function does not change either polygon, but rather create a new one.
other
merged
nil
if the two polygons don't share an edge.merged = p1:mergedWith(p2)
Test if the polygon contains a given point.
x, y
true
if x,y
lies in the interior of the polygon.if button:contains(love.mouse.getPosition) then
button:setHovered(true)
end
Test if the polygon intersects a ray.
x, y
dx, dy
intersection
true
if the given ray intersects the shape.t
(only if intersecting)if poly:intersectsRay(400,300, dx,dy) then
love.graphics.setLine(2) -- highlight polygon
end
spatialhash = require "hardoncollider.spatialhash"
A spatial hash implementation that supports scenes of arbitrary size. The hash is sparse, which means that cells will only be created when needed.
Syntax depends on used class system. Shown syntax works for bundled hump.class and slither.
Create a new spatial hash given a cell size.
Choosing a good cell size depends on your application. To get a decent speedup, the average cell should not contain too many objects, nor should a single object occupy too many cells. A good rule of thumb is to choose the cell size so that the average object will occupy one cell only.
cell_size
(100)Spatialhash = require 'hardoncollider.spatialhash'
hash = Spatialhash(150)
Get coordinates of a given value, i.e. the cell index in which a given point would be placed.
x, y
x,y
.local mx,my = love.mouse.getPosition()
cx, cy = hash:cellCoords(mx, my)
Get the cell with given coordinates.
A cell is a table which's keys and value are the objects stored in the cell, i.e.:
cell = {
[obj1] = obj1,
[obj2] = obj2,
...
}
You can iterate over the objects in a cell using pairs()
:
for object in pairs(cell) do stuff(object) end
i, k
local mx,my = love.mouse.getPosition()
cx, cy = hash:cellCoords(mx, my)
cell = hash:cell(cx, cy)
Get the cell that contains point x,y
.
Same as hash:cell(hash:cellCoords(x,y))
x, y
local mx,my = love.mouse.getPosition()
cell = hash:cellAt(mx, my)
Insert an object into the hash using a given bounding box.
obj
nil
.x1,y1
x2,y2
hash:insert(shape, shape:bbox())
Remove an object from the hash using a bounding box.
If no bounding box is given, search the whole hash to delete the object.
obj
x1,y1
x2,y2
hash:remove(shape, shape:bbox())
hash:remove(object_with_unknown_position)
Update an objects position given the old bounding box and the new bounding box.
obj
x1,y1
x2,y2
x3,y3
x4,y4
hash:update(shape, -100,-30, 0,60, -100,-70, 0,20)
Query objects in the rectangle (x1,y1) - (x2,y2)
.
x1, y1
x2, y2
t[obj] = obj
) of objects.local objects = hash:inRange(0,0, 800,600)
for obj in pairs(objects) do
obj:draw()
end
Iterator to objects in the rectangle (x1,y1) - (x2,y2)
.
Alias to pairs(hash:inRange(x1,y1, x2,y2))
.
x1, y1
x2, y2
for obj in hash:rangeIter(0,0, 800,600) do
obj:draw()
end
Draw hash cells on the screen, mostly for debug purposes
draw_mode
show_empty
(true)print_key
(false)love.graphics.setColor(160,140,100,100)
hash:draw('line', true, true)
hash:draw('fill', false)
require "hardoncollider.vector-light"
require "hardoncollider.class"
See hump.class