collision2d
stable2D collision detection library with points, rectangles, circles, polygons, lines, swept shapes, ray casting, and broad-phase grid queries.
use plugin collision2d::{point_in_rect, point_in_circle, rect_rect, …} Functions (22)
- point_in_rect Test if a point is inside a rectangle
- point_in_circle Test if a point is inside a circle
- rect_rect Test if two rectangles overlap
- circle_circle Test if two circles overlap
- rect_circle Test if a rectangle and circle overlap
- line_line Test if two line segments intersect
- line_rect Test if a line segment intersects a rectangle
- line_circle Test if a line segment intersects a circle
- point_in_polygon Test if a point is inside a polygon
- sat_test SAT collision test for convex polygons
- sweep_circle_rect Swept circle vs AABB collision
- aabb_sweep Swept AABB vs static AABB collision
- gjk_test GJK collision test for convex polygons
- minkowski_sum Compute Minkowski sum of two convex polygons
- closest_point_on_line Find closest point on a line segment
- closest_point_on_polygon Find closest point on a polygon
- ray_cast Cast a ray against a list of shapes
- broad_phase_grid Spatial hash for broad-phase collision pairs
- distance Euclidean distance between two points
- distance_point_rect Shortest distance from a point to a rectangle
- rect_contains_rect Test if one rectangle fully contains another
- circle_contains_point Test if a circle contains a point
Overview
collision2d is a stateless, pure-function library for 2D geometric collision
detection — the kind of math a game loop runs every frame. There are no handles
or objects to manage: every function takes raw coordinates (or a table of
{x, y} points for polygons) and returns either a bool or a result table.
Coordinates are plain numbers in whatever unit your game uses, and rectangles
are always axis-aligned, described as origin (x, y) plus size (w, h).
Reach for the simple boolean overlap tests (rect_rect, circle_circle,
point_in_rect) for cheap narrow-phase checks, the swept tests
(aabb_sweep, sweep_circle_rect) for continuous collision of fast movers,
and the convex-polygon tools (sat_test, gjk_test, minkowski_sum) when you
need arbitrary shapes or a minimum-translation vector to resolve a hit. For
many bodies, run broad_phase_grid first to cull pairs, then only run the
expensive narrow-phase test on the candidates it returns.
Common patterns
Broad-phase then narrow-phase
Cull obviously-distant pairs with the spatial grid, then confirm each candidate
with a precise rect_rect test.
use plugin collision2d::{broad_phase_grid, rect_rect}
let shapes = #{
1: #{"x": 0.0, "y": 0.0, "w": 5.0, "h": 5.0},
2: #{"x": 3.0, "y": 3.0, "w": 5.0, "h": 5.0},
3: #{"x": 40.0, "y": 40.0, "w": 5.0, "h": 5.0}
}
let pairs = broad_phase_grid(shapes, 10.0)
for pair in pairs {
let a = shapes[pair["a"]]
let b = shapes[pair["b"]]
if rect_rect(a["x"], a["y"], a["w"], a["h"], b["x"], b["y"], b["w"], b["h"]) {
print("real collision between {pair["a"]} and {pair["b"]}")
}
}
Resolve an overlap with SAT
When two convex polygons overlap, sat_test hands back the minimum-translation
axis and depth so you can push one shape out of the other.
use plugin collision2d::{sat_test}
let a = #{1: #{"x": 0.0, "y": 0.0}, 2: #{"x": 5.0, "y": 0.0},
3: #{"x": 5.0, "y": 5.0}, 4: #{"x": 0.0, "y": 5.0}}
let b = #{1: #{"x": 4.0, "y": 1.0}, 2: #{"x": 9.0, "y": 1.0},
3: #{"x": 9.0, "y": 6.0}, 4: #{"x": 4.0, "y": 6.0}}
let mtv = sat_test(a, b)
if mtv["hit"] {
let push_x = mtv["axis_x"] * mtv["overlap"]
let push_y = mtv["axis_y"] * mtv["overlap"]
print("separate by ({push_x}, {push_y})")
}
Continuous collision for a fast mover
A fast projectile can tunnel through a wall in a single frame. Sweep it instead
and use the returned time to stop it at the contact point.
use plugin collision2d::{aabb_sweep}
let vx = 20.0
let vy = 0.0
let result = aabb_sweep(0.0, 0.0, 2.0, 2.0, vx, vy, 10.0, 0.0, 4.0, 4.0)
if result["hit"] {
let stop_x = vx * result["time"]
print("stop after {stop_x} units, hit normal ({result["nx"]}, {result["ny"]})")
}
Test if a point is inside a rectangle
Returns true if the point (px, py) lies inside the axis-aligned rectangle defined by origin (rx, ry) and dimensions (rw, rh).
use plugin collision2d::{point_in_rect}
let inside = point_in_rect(5.0, 5.0, 0.0, 0.0, 10.0, 10.0)
print("inside: {inside}")
let outside = point_in_rect(15.0, 5.0, 0.0, 0.0, 10.0, 10.0)
print("outside: {outside}")
Test if a point is inside a circle
Returns true if the point (px, py) is within radius r of circle center (cx, cy).
use plugin collision2d::{point_in_circle}
let hit = point_in_circle(3.0, 4.0, 0.0, 0.0, 5.0)
print("in circle: {hit}")
Test if two rectangles overlap
Returns true if two axis-aligned rectangles overlap (AABB vs AABB).
use plugin collision2d::{rect_rect}
let overlapping = rect_rect(0.0, 0.0, 10.0, 10.0, 5.0, 5.0, 10.0, 10.0)
print("overlap: {overlapping}")
let separate = rect_rect(0.0, 0.0, 5.0, 5.0, 10.0, 0.0, 5.0, 5.0)
print("no overlap: {separate}")
Test if two circles overlap
Returns true if two circles overlap. Compares distance between centers against the sum of radii.
use plugin collision2d::{circle_circle}
let hit = circle_circle(0.0, 0.0, 5.0, 8.0, 0.0, 5.0)
print("circles overlap: {hit}")
Two circles that exactly touch (distance equals the sum of radii) count as overlapping:
use plugin collision2d::{circle_circle}
let touching = circle_circle(0.0, 0.0, 5.0, 10.0, 0.0, 5.0)
print("touching counts as hit: {touching}")
Test if a rectangle and circle overlap
Returns true if a rectangle and a circle overlap, using the closest-point-on- AABB method.
use plugin collision2d::{rect_circle}
let hit = rect_circle(0.0, 0.0, 10.0, 10.0, 12.0, 5.0, 3.0)
print("rect-circle overlap: {hit}")
Test if two line segments intersect
Tests if two line segments intersect. Returns a table with hit (bool),
x, and y. If hit is true, x and y are the intersection point.
use plugin collision2d::{line_line}
let result = line_line(0.0, 0.0, 10.0, 10.0, 0.0, 10.0, 10.0, 0.0)
if result["hit"] {
print("intersection at {result["x"]}, {result["y"]}")
}
Test if a line segment intersects a rectangle
Returns true if a line segment intersects any of the four edges of a rectangle.
use plugin collision2d::{line_rect}
let hit = line_rect(5.0, -5.0, 5.0, 15.0, 0.0, 0.0, 10.0, 10.0)
print("line hits rect: {hit}")
Test if a line segment intersects a circle
Returns true if a line segment passes through or touches a circle, using closest-point projection onto the segment.
use plugin collision2d::{line_circle}
let hit = line_circle(0.0, 0.0, 10.0, 0.0, 5.0, 3.0, 4.0)
print("line hits circle: {hit}")
Test if a point is inside a polygon
Tests if a point is inside a polygon using the ray casting algorithm.
polygon is a table of {x, y} tables.
use plugin collision2d::{point_in_polygon}
let poly = #{
1: #{"x": 0.0, "y": 0.0},
2: #{"x": 10.0, "y": 0.0},
3: #{"x": 10.0, "y": 10.0},
4: #{"x": 0.0, "y": 10.0}
}
let inside = point_in_polygon(5.0, 5.0, poly)
print("inside polygon: {inside}")
SAT collision test for convex polygons
Separating Axis Theorem test for two convex polygons. Returns a table with
hit (bool), overlap (float), axis_x, and axis_y (the minimum
translation axis).
use plugin collision2d::{sat_test}
let a = #{1: #{"x": 0.0, "y": 0.0}, 2: #{"x": 5.0, "y": 0.0},
3: #{"x": 5.0, "y": 5.0}, 4: #{"x": 0.0, "y": 5.0}}
let b = #{1: #{"x": 3.0, "y": 3.0}, 2: #{"x": 8.0, "y": 3.0},
3: #{"x": 8.0, "y": 8.0}, 4: #{"x": 3.0, "y": 8.0}}
let result = sat_test(a, b)
print("hit: {result["hit"]}, overlap: {result["overlap"]}")
Swept circle vs AABB collision
Swept circle vs AABB. Returns hit, time (0–1), normal_x, normal_y.
Use for continuous collision detection of fast-moving round objects.
use plugin collision2d::{sweep_circle_rect}
let result = sweep_circle_rect(0.0, 5.0, 1.0, 10.0, 0.0, 8.0, 0.0, 5.0, 10.0)
if result["hit"] {
print("collision at time {result["time"]}")
}
Swept AABB vs static AABB collision
Swept AABB vs static AABB. Returns hit, time, nx, and ny (collision
normal). Use for moving box entities against static walls.
use plugin collision2d::{aabb_sweep}
let result = aabb_sweep(0.0, 0.0, 2.0, 2.0, 5.0, 0.0, 4.0, 0.0, 4.0, 4.0)
print("hit: {result["hit"]}, time: {result["time"]}")
GJK collision test for convex polygons
Gilbert–Johnson–Keerthi algorithm for convex polygon overlap. More robust than SAT for complex shapes. Returns true if the polygons overlap.
use plugin collision2d::{gjk_test}
let tri = #{1: #{"x": 0.0, "y": 0.0}, 2: #{"x": 6.0, "y": 0.0},
3: #{"x": 3.0, "y": 6.0}}
let box = #{1: #{"x": 2.0, "y": 2.0}, 2: #{"x": 5.0, "y": 2.0},
3: #{"x": 5.0, "y": 5.0}, 4: #{"x": 2.0, "y": 5.0}}
print("gjk hit: {gjk_test(tri, box)}")
gjk_test returns a plain boolean, so it works well as a quick guard before
running a more expensive sat_test to extract the actual separation:
use plugin collision2d::{gjk_test, sat_test}
let a = #{1: #{"x": 0.0, "y": 0.0}, 2: #{"x": 4.0, "y": 0.0},
3: #{"x": 4.0, "y": 4.0}, 4: #{"x": 0.0, "y": 4.0}}
let b = #{1: #{"x": 2.0, "y": 2.0}, 2: #{"x": 6.0, "y": 2.0},
3: #{"x": 6.0, "y": 6.0}, 4: #{"x": 2.0, "y": 6.0}}
if gjk_test(a, b) {
let mtv = sat_test(a, b)
print("overlap depth: {mtv["overlap"]}")
}
Compute Minkowski sum of two convex polygons
Computes the Minkowski sum of two convex polygons. Returns a table of {x, y}
points representing the resulting polygon.
use plugin collision2d::{minkowski_sum}
let a = #{1: #{"x": 0.0, "y": 0.0}, 2: #{"x": 2.0, "y": 0.0},
3: #{"x": 2.0, "y": 2.0}, 4: #{"x": 0.0, "y": 2.0}}
let b = #{1: #{"x": 0.0, "y": 0.0}, 2: #{"x": 1.0, "y": 0.0},
3: #{"x": 0.0, "y": 1.0}}
let sum = minkowski_sum(a, b)
print("result has {#sum} vertices")
Find closest point on a line segment
Finds the closest point on a line segment to a given point. Returns a table
with x, y, and dist fields.
use plugin collision2d::{closest_point_on_line}
let result = closest_point_on_line(3.0, 5.0, 0.0, 0.0, 10.0, 0.0)
print("closest: ({result["x"]}, {result["y"]}), dist: {result["dist"]}")
Find closest point on a polygon
Finds the closest point on any edge of a polygon to a given point. Returns
x, y, dist, and edge_index (0-based index of the closest edge).
use plugin collision2d::{closest_point_on_polygon}
let poly = #{1: #{"x": 0.0, "y": 0.0}, 2: #{"x": 10.0, "y": 0.0},
3: #{"x": 10.0, "y": 10.0}, 4: #{"x": 0.0, "y": 10.0}}
let result = closest_point_on_polygon(15.0, 5.0, poly)
print("closest at ({result["x"]}, {result["y"]})")
Cast a ray against a list of shapes
Casts a ray from origin (ox, oy) in direction (dx, dy) against a table of
shapes. Each shape needs type ("rect" or "circle") plus position/size
fields. Returns hit, x, y, dist, and shape_index (1-based).
use plugin collision2d::{ray_cast}
let shapes = #{
1: #{"type": "rect", "x": 5.0, "y": 0.0, "w": 3.0, "h": 3.0},
2: #{"type": "circle", "cx": 15.0, "cy": 1.0, "r": 2.0}
}
let result = ray_cast(0.0, 1.0, 1.0, 0.0, shapes)
if result["hit"] {
print("hit shape {result["shape_index"]} at dist {result["dist"]}")
}
The ray returns the nearest hit, so it works as a line-of-sight check: cast toward a target and confirm the first thing the ray touches is the target itself.
use plugin collision2d::{ray_cast}
let world = #{
1: #{"type": "rect", "x": 4.0, "y": -1.0, "w": 1.0, "h": 3.0},
2: #{"type": "circle", "cx": 10.0, "cy": 0.0, "r": 1.0}
}
let shot = ray_cast(0.0, 0.0, 1.0, 0.0, world)
let blocked = shot["hit"] && shot["shape_index"] != 2
print("target {shot["shape_index"]} blocked: {blocked}")
Spatial hash for broad-phase collision pairs
Spatial hash broad-phase: returns pairs {a, b} (1-based shape indices)
that share a grid cell and should be checked for narrow-phase collision.
use plugin collision2d::{broad_phase_grid}
let shapes = #{
1: #{"x": 0.0, "y": 0.0, "w": 5.0, "h": 5.0},
2: #{"x": 3.0, "y": 3.0, "w": 5.0, "h": 5.0},
3: #{"x": 50.0, "y": 50.0, "w": 5.0, "h": 5.0}
}
let pairs = broad_phase_grid(shapes, 10.0)
print("candidate pairs: {#pairs}")
Euclidean distance between two points
Returns the Euclidean distance between two 2D points.
use plugin collision2d::{distance}
let d = distance(0.0, 0.0, 3.0, 4.0)
print("distance: {d}")
Combine distance with a radius to build a simple proximity test, such as an
enemy aggro range:
use plugin collision2d::{distance}
let player_x = 12.0
let player_y = 9.0
let enemy_x = 10.0
let enemy_y = 6.0
let aggro_range = 5.0
if distance(player_x, player_y, enemy_x, enemy_y) <= aggro_range {
print("enemy is now chasing")
}
Shortest distance from a point to a rectangle
Returns the shortest distance from point (px, py) to the surface of an axis-aligned rectangle. Returns 0 if the point is inside the rectangle.
use plugin collision2d::{distance_point_rect}
let d = distance_point_rect(15.0, 5.0, 0.0, 0.0, 10.0, 10.0)
print("distance to rect: {d}")
Test if one rectangle fully contains another
Returns true if the second rectangle is fully contained within the first.
use plugin collision2d::{rect_contains_rect}
let contained = rect_contains_rect(0.0, 0.0, 20.0, 20.0, 5.0, 5.0, 5.0, 5.0)
print("contained: {contained}")
Test if a circle contains a point
Returns true if the point (px, py) lies within the circle defined by center (cx, cy) and radius cr.
use plugin collision2d::{circle_contains_point}
let inside = circle_contains_point(0.0, 0.0, 10.0, 6.0, 8.0)
print("inside: {inside}")