Collision Detection

Axis-aligned bounding box (AABB) and circle collision in Chasm.

Collision detection in game programming comes in two main flavors: AABB (axis-aligned bounding boxes, rectangles that don't rotate) and circle collision. Both are fast to compute and cover the majority of arcade and action game needs.

AABB Collision

Two rectangles overlap if neither is fully to the left, right, above, or below the other. A rectangle is described by its top-left corner (x, y) and its dimensions (w, h).

defp aabb(ax :: float, ay :: float, aw :: float, ah :: float,
          bx :: float, by :: float, bw :: float, bh :: float) :: int do
  if ax + aw <= bx do return 0 end   # a is left of b
  if bx + bw <= ax do return 0 end   # b is left of a
  if ay + ah <= by do return 0 end   # a is above b
  if by + bh <= ay do return 0 end   # b is above a
  return 1
end

Usage:

if aabb(@player_x, @player_y, 32.0, 32.0,
        coin_x, coin_y, 16.0, 16.0) == 1 do
  collect_coin()
end

Why This Works

The early-return pattern checks four separating axes. If the rectangles are separated on any axis, they cannot overlap. Only when all four checks pass do we know they intersect.

This is the same underlying logic as the Separating Axis Theorem (SAT), just simplified for axis-aligned boxes.

Circle Collision

Two circles overlap when the distance between their centers is less than the sum of their radii. Computing a square root is expensive, so we compare squared distances instead, mathematically identical, no sqrt needed.

defp circle_overlap(ax :: float, ay :: float, ar :: float,
                    bx :: float, by :: float, br :: float) :: int do
  dx = ax - bx
  dy = ay - by
  dist_sq = dx * dx + dy * dy
  sum_r = ar + br
  if dist_sq < sum_r * sum_r do
    return 1
  end
  return 0
end

Usage:

if circle_overlap(@player_x, @player_y, 16.0,
                  enemy_x, enemy_y, 20.0) == 1 do
  take_damage()
end

Combining with Object Pools

Check every active bullet against every active enemy:

defp check_bullet_enemy_collisions() do
  bi = 0
  while bi < @bullets.len do
    b = @bullets.get(bi)
    if b.active == 1 do
      ei = 0
      while ei < @enemies.len do
        e = @enemies.get(ei)
        if e.active == 1 do
          if aabb(b.x - 3.0, b.y - 6.0, 6.0, 12.0,
                  e.x - e.radius, e.y - e.radius,
                  e.radius * 2.0, e.radius * 2.0) == 1 do
            # Hit, deactivate both
            @bullets.set(bi, b with { active: 0 })
            @enemies.set(ei, e with { hp: e.hp - 1 })
          end
        end
        ei = ei + 1
      end
    end
    bi = bi + 1
  end
end

Note: this is O(bullets × enemies). For small counts (< 100 each) this is fast. For larger counts, spatial partitioning (grids, quadtrees) reduces it to near-linear.

Resolving Overlap

Detection tells you if objects overlap. Resolution moves them apart. For player–wall collision, push the player out by the overlap amount:

defstruct Rect do
  x :: float = 0.0
  y :: float = 0.0
  w :: float = 0.0
  h :: float = 0.0
end

defp resolve_aabb(player :: Rect, wall :: Rect) :: Rect do
  # Only resolve if overlapping
  if aabb(player.x, player.y, player.w, player.h,
          wall.x, wall.y, wall.w, wall.h) == 0 do
    return player
  end

  # Find overlap depth on each axis
  overlap_left  = (player.x + player.w) - wall.x
  overlap_right = (wall.x + wall.w) - player.x
  overlap_top   = (player.y + player.h) - wall.y
  overlap_bot   = (wall.y + wall.h) - player.y

  # Push out on the shallowest axis
  min_x = overlap_left
  if overlap_right < min_x do min_x = overlap_right end
  min_y = overlap_top
  if overlap_bot   < min_y do min_y = overlap_bot   end

  if min_x < min_y do
    # Horizontal push
    if overlap_left < overlap_right do
      return player with { x: player.x - overlap_left }
    end
    return player with { x: player.x + overlap_right }
  end

  # Vertical push
  if overlap_top < overlap_bot do
    return player with { y: player.y - overlap_top }
  end
  return player with { y: player.y + overlap_bot }
end

Tile Maps

For a grid-based map, only check tiles near the player instead of every tile:

@tile_size :: script = 32.0
@map_w     :: script = 20
@map_h     :: script = 15
@tiles     :: script = array_fixed(300, 0)   # 0 = empty, 1 = solid

defp is_solid(tx :: int, ty :: int) :: int do
  if tx < 0 or tx >= @map_w or ty < 0 or ty >= @map_h do
    return 1   # out of bounds = solid wall
  end
  return @tiles.get(ty * @map_w + tx)
end

defp collide_player_tiles(px :: float, py :: float, pw :: float, ph :: float) :: float do
  # Check the tile columns the player overlaps
  left_tile  = to_int(px / @tile_size)
  right_tile = to_int((px + pw - 1.0) / @tile_size)
  top_tile   = to_int(py / @tile_size)
  bot_tile   = to_int((py + ph - 1.0) / @tile_size)

  tx = left_tile
  while tx <= right_tile do
    ty = top_tile
    while ty <= bot_tile do
      if is_solid(tx, ty) == 1 do
        tile_x = to_float(tx) * @tile_size
        tile_y = to_float(ty) * @tile_size
        # resolve and update px, py here
      end
      ty = ty + 1
    end
    tx = tx + 1
  end
  return px
end

Tile collision is fast because you only check the 4–9 tiles the player could possibly touch.

Point-in-Rectangle

Useful for UI clicks or checking if a projectile origin is inside an area:

defp point_in_rect(px :: float, py :: float,
                   rx :: float, ry :: float, rw :: float, rh :: float) :: int do
  if px >= rx and px <= rx + rw and py >= ry and py <= ry + rh do
    return 1
  end
  return 0
end

Example, button click detection:

if mouse_pressed(:left) do
  if point_in_rect(mouse_x(), mouse_y(), 100.0, 200.0, 160.0, 40.0) == 1 do
    start_game()
  end
end