Cache distinct separating axes per hit box - #2895
Merged
Merged
Conversation
After the quick distance check, _check_for_collision now compares the cached (left, right, bottom, top) bounds of both hit boxes before running the separating axis test. Because the bounds check already covers the x and y axes, the SAT skips horizontal and vertical edges. Two axis-aligned boxes need no SAT at all, and the default octagon hit boxes only test their 4 diagonal edges. - Add HitBox.get_adjusted_bounds(), cached against the adjusted point list so subclasses overriding get_adjusted_points() stay correct. - are_polygons_intersecting() does its own bounding box check and uses the same SAT helper. - Skipping zero-length edges fixes polygons with repeated points (e.g. closed polygons) never intersecting. - left/right/bottom/top stay uncached: using the cached bounds there made reading a single bound after a move ~50% slower. Checks that reach the polygon test are about 2-4x faster. New tests compare results with the original SAT on thousands of random polygons and sprite pairs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each hit box now caches its distinct, non axis-aligned edge normals. Parallel edges (opposite sides of the default octagon hit boxes, or matching edges on two sprites with the same angle) are tested once, by merging the two hit boxes' axis dicts. The directions are worked out once from the raw points, then scaled and rotated when scale or angle changes. They don't depend on position, so moving sprites keep the cache. Subclasses that override get_adjusted_points() fall back to axes from the adjusted points. Axes are keyed by rounded unit vectors, but projections use the unnormalized normals so exact touches stay exact. Only exactly axis-aligned normals are skipped. Right-angle rotations now use exact cosine and sine values, so hit boxes at 90/180/270 degrees get exact coordinates instead of ~1e-16 errors that could change the result for exactly touching sprites. The random reference test now includes a detailed (possibly concave) hit box and same-angle pairs. Its reference also checks the x and y axes, since testing only edge normals misses some separations for concave shapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-axes # Conflicts: # CHANGELOG.md # arcade/hitbox/base.py # arcade/sprite_list/collision.py # tests/unit/hitbox/test_hitbox.py # tests/unit/sprite/test_sprite_collision.py
sin(pi / 4) is 0.7071067811865476 on Windows but 0.7071067811865475 on Linux (glibc). At 45 degrees the rotated octagon diagonals only cancel to exactly axis-aligned normals on Windows; on Linux they're ~1e-16 off axis and are kept as axes to test. Both are correct, so the test now accepts either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pvcraven
added a commit
that referenced
this pull request
Oct 1, 2026
These are cleaned-up versions of the scripts used to measure and check the recent collision changes (#2892-#2895): - collisions/micro.py: per-call timing of check_for_collision on single pairs, on sprites moving or rotating every call, and of check_for_collision_with_list. - collisions/hit_box.py: hit box bounds properties and adjusted point recalculation. - collisions/compare_reference.py: correctness check comparing check_for_collision with a simple reference on random sprite pairs. Exits with an error on any mismatch. - spatial_hash/queries.py: query timing, and a check that queries don't grow the spatial hash. Add a README for the collisions folder covering what each script does and how to compare two versions. Co-authored-by: Paul V Craven <paul.craven@optimizley.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
After #2894, the polygon test still projected onto every non axis-aligned edge normal of both hit boxes, about 1 µs per axis. Many of those axes are duplicates:
This PR caches each hit box's distinct edge directions and tests each direction only once per pair.
How it works
HitBox._get_axes()returns a dict mapping a rounded unit direction to an edge normal. Merging the two hit boxes' dicts (axes1 | axes2) drops directions they share.get_adjusted_points(), the axes come from its adjusted points instead of assuming the built-in transform.(4.5, -2.25)), which keeps exact arithmetic for common sizes and positions. An earlier version projected onto unit normals, and rounding then turned some exact touches into collisions.Right angles now give exact coordinates
sin(radians(180))is about 1.2e-16, not 0, so a hit box rotated 90°/180°/270° had coordinates like15.999999999999998. Whether two exactly touching sprites collided could depend on that noise, and it differed from the unrotated case.RotatableHitBoxnow uses exact cosine/sine values for multiples of 90°. This was needed so that nearly axis-aligned normals don't appear at right angles, but it's also a small accuracy improvement on its own (e.g.sprite.lefton a tile rotated 90° is now exact).Benchmarks
µs per call, best of 5, same script against
perf/collision-aabb(#2894) and this branch.Even when the cache is rebuilt on every call (rotating sprites), it's still faster than before.
get_adjusted_points()recompute cost: angle 0 is ~6% faster (sine/cosine are no longer computed when unused), 90° is unchanged, and other angles are ~0.1 µs (~4%) slower because of the right-angle check.Correctness
test_check_for_collision_matches_referencenow also checks the x and y axes, and the test now includes the detailed meteor hit box and same-angle pairs.test_draw_primitives,test_render_sprite_solid_pixels,test_get_image) also fail ondevelopmenton my machine. Ruff is clean, and mypy reports no errors in the changed files.Known approximation
Two edges count as parallel if their unit directions agree to 9 decimal places, so a pair of edges less than ~1e-9 radians apart shares one test. That could only change the result when the sprites are within about a billionth of their size of touching.
🤖 Generated with Claude Code