Skip to content

collision2d

stable

2D 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, …}
22 functions Game
/ filter jk navigate Esc clear
Functions (22)
  1. point_in_rect Test if a point is inside a rectangle
  2. point_in_circle Test if a point is inside a circle
  3. rect_rect Test if two rectangles overlap
  4. circle_circle Test if two circles overlap
  5. rect_circle Test if a rectangle and circle overlap
  6. line_line Test if two line segments intersect
  7. line_rect Test if a line segment intersects a rectangle
  8. line_circle Test if a line segment intersects a circle
  9. point_in_polygon Test if a point is inside a polygon
  10. sat_test SAT collision test for convex polygons
  11. sweep_circle_rect Swept circle vs AABB collision
  12. aabb_sweep Swept AABB vs static AABB collision
  13. gjk_test GJK collision test for convex polygons
  14. minkowski_sum Compute Minkowski sum of two convex polygons
  15. closest_point_on_line Find closest point on a line segment
  16. closest_point_on_polygon Find closest point on a polygon
  17. ray_cast Cast a ray against a list of shapes
  18. broad_phase_grid Spatial hash for broad-phase collision pairs
  19. distance Euclidean distance between two points
  20. distance_point_rect Shortest distance from a point to a rectangle
  21. rect_contains_rect Test if one rectangle fully contains another
  22. 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}")
enespt-br