Skip to content

Add get_collision_info for push-out direction and depth - #2910

Merged
pvcraven merged 1 commit into
developmentfrom
feature/collision-info
Oct 1, 2026
Merged

pvcraven merged 1 commit into
developmentfrom
feature/collision-info

Conversation

@pvcraven

@pvcraven pvcraven commented Oct 1, 2026

Copy link
Copy Markdown
Member

Summary

Adds arcade.get_collision_info(sprite1, sprite2), which returns how two colliding sprites overlap: the smallest move that separates them (the "minimum translation vector").

info = arcade.get_collision_info(player, wall)   # None if not colliding
if info:
    player.position += info.normal * info.depth  # now just touching

CollisionInfo is a NamedTuple:

  • normal: a unit pyglet.math.Vec2 pointing the way to move sprite1. Swapping the sprites flips it.
  • depth: how far to move, in pixels. Always > 0.

player.position += info.normal * info.depth works even though sprite positions are plain tuples: Vec2 subclasses tuple, so Python uses Vec2.__radd__.

This is PR 1 of the plan discussed for "push-out direction and depth." A list version and optional use in the physics engines would come later, as separate PRs.

How it works

It uses the same stages as check_for_collision:

  1. Distance check with the cached hit box radii.
  2. Bounding-box check. The bounding boxes also give the y- and x-axis overlaps directly.
  3. Separating axis test on the cached distinct edge normals. Each axis now records the overlap in both directions instead of just yes/no.

It returns the smallest overlap. Because the cached normals aren't unit length (that keeps exact touches exact), each overlap is divided by the normal's length to get pixels.

check_for_collision is untouched. get_collision_info makes the same separation comparisons on the same axes, so it returns None exactly when check_for_collision returns False, including that touching isn't colliding.

Behavior notes, all documented

  • Ties: when several moves are equally small, the y axis is preferred, then x, then other edges; and up or right over down or left. So two identical sprites in the same place separate with sprite1 moving up.
  • Rounding: after moving exactly normal * depth the hit boxes touch, but floating point rounding can leave them overlapping by a tiny amount. The docstring suggests adding a small extra distance if that matters.
  • Convex only: the result is only correct for convex hit boxes. The detailed hit box algorithm can make concave ones, and those already give false-positive collisions today.

Tests

  • test_get_collision_info_basics: exact x overlap and normal, swapped sprites flipping the normal, sinking into a floor (pushed up exactly 7 px and no longer colliding afterwards), identical sprites in the same place (pushed up), and touching or far apart (None).
  • test_get_collision_info_diagonal: a 45° box gives a diagonal unit normal.
  • test_get_collision_info_matches_check_for_collision: 3,000 random pairs, including concave detailed hit boxes, in both argument orders. None exactly when check_for_collision is False.
  • test_get_collision_info_separates: 3,000 random convex pairs (rotated, flipped, scaled). For each collision:
    • the normal is unit length,
    • depth matches a brute-force minimum over every edge normal plus x/y,
    • moving depth + 1e-6 separates the sprites, and moving depth - 1e-6 doesn't.
  • Type errors, including a SpriteList passed as the second argument.
  • Deliberate bugs are caught: not converting overlaps to pixels, pushing the wrong way along edge normals, and breaking the tie-break each make the tests fail.
  • Stress check (not committed): 100,000 random pairs. No disagreements with check_for_collision, and among 25,136 convex collisions no depth mismatches and no failed separations.
  • The collision tests also pass with Linux's sin(π/4) value simulated, so the results don't depend on the platform. Full suite on pyglet 3.0.dev11: 1424 passed. The 3 failures are the render tests that only fail on my machine.

Performance

benchmarks/collisions/micro.py now times get_collision_info on the same pairs as check_for_collision. µs per call, from a clean run:

Pair check_for_collision get_collision_info
box / box, overlapping 1.53 2.09
octagon / octagon, overlapping 4.02 4.66
octagon / octagon rotated 30°, overlapping 5.86 7.01
octagon / octagon, near-miss 1.13 1.09
far apart 0.30 0.39

So it's about 1.2–1.4× the cost of a yes/no check when sprites overlap, and the same otherwise.

🤖 Generated with Claude Code

get_collision_info(sprite1, sprite2) returns a CollisionInfo with the
smallest move that separates two colliding sprites: a unit normal (the
direction to move sprite1) and a depth in pixels. It returns None
exactly when check_for_collision returns False.

It uses the same distance, bounding box, and separating axis stages as
check_for_collision, keeping the overlap on each axis instead of only
checking for separation. The bounding boxes give the x and y overlaps;
the cached edge normals give the rest, divided by their length to get
pixels. Ties prefer the y axis, then x, then other edges, and up or
right over down or left. Correct for convex hit boxes.

check_for_collision is unchanged. get_collision_info costs about
1.2-1.4x as much for overlapping sprites and the same otherwise. Add it
to benchmarks/collisions/micro.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pvcraven
pvcraven merged commit e765675 into development Oct 1, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant