Skip to content
Plugins (190)

collision2d

stable

2D collision detection library with points, rectangles, circles, polygons, lines, swept shapes, ray casting, and broad-phase grid queries.

use plugin collision2d
22 functions Game
Runs with the native Zolo host

Plugin snippets can be edited in the Playground, but native capabilities are executed locally with the Zolo CLI.

/ 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

From import to result

  1. 01Import only what you use
  2. 02Copy the smallest example
  3. 03Run it with the Zolo CLI

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 {len(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: {len(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}")

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

en