Move sprites out of walls exactly in the physics engines - #2912
Merged
Merged
Conversation
PhysicsEngineSimple and PhysicsEnginePlatformer resolved collisions with step-by-step searches: a "wiggle" search for a sprite starting inside a wall or rotating into one, 1 px steps down from a ceiling, and 0.25 px steps up from a floor or ramp, with a full collision check per step. - Starting overlaps and rotation now move the sprite out of the deepest wall along the smallest distance (get_collision_info_with_list), repeated up to 8 times for several walls. - Vertical collisions now move the sprite exactly up or down out of the wall, using a new private _get_separation_distance(): the distance along a fixed direction to separate two sprites, from the same separating axis test. - If the exact method doesn't free the sprite (wedged between walls, or concave hit boxes), the old search runs from the original position. - The engines round center_y to 2 decimal places each update; that's kept, unless it would move the sprite back into a wall it was just moved out of. Also fix an existing bug: after rotating out of a wall, the x move was measured from the position before rotating, undoing any sideways move out of the wall. Moving and rotating sprites could end updates inside walls. Behavior changes: sprites stop exactly at floors, ramps, and ceilings instead of up to 0.25 px or 1 px away, and rotating into a wall moves the sprite the exact distance needed instead of a whole number of pixels. Six rotation assertions in test_physics_engine2.py are updated to the exact values; all its other pinned positions are unchanged. 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
PR 3 of the push-out plan (after #2910 and #2911).
PhysicsEngineSimpleandPhysicsEnginePlatformernow move sprites out of walls exactly instead of with step-by-step searches. This is faster and more precise. It also fixes an existing bug where a sprite rotating and moving sideways could end an update inside a wall.This changes engine behavior slightly (details below), so please review with that in mind.
What changed
get_collision_info_with_list), up to 8 times for several wallscollision._get_separation_distance(a, b, dx, dy), which gives the exact distance to moveaalong a fixed direction until it stops colliding withb. It uses the same separating axis test, with the cached edge normals.Rounding
The engines already round
center_yto 2 decimal places on every update; that's kept. An exact landing can sit at y = 47.1234, so rounding could move the sprite 0.003 px back into the wall it was just moved out of. Rounding is now skipped when that would happen.I first missed this case after a rotation push-out. The 45° rotation test caught it, and the guard covers every collision resolved in the update.
Existing bug fixed
_move_spriterecordsoriginal_xbefore rotating, and the x move then setscenter_x = original_x + change_x. So any sideways move made to get out of a wall after rotating was undone, and the sprite could end the update inside the wall. The old wiggle search usually escaped vertically, so this was rare: 3 of 1,200 frames in my top-down run. With smallest-distance moves it showed up in 19 frames. The x move is now measured from where the rotation step left the sprite, and the same run has 0.Behavior changes
test_physics_engine2.pypinned the old wiggle results. I updated them to the exact values (computed from the geometry, e.g.5√2 − 5for 45°) and they now also assert no overlap. Every other pinned position in that file is unchanged: moving into walls from all four sides, the starting-overlap case, the platformer ramp steps, and free fall.Comparison with the old engine
I ran a scripted 1,500-frame platformer session (walking, jumping into a low ceiling, landing, a rotated-tile ramp, a step, a moving platform) and a 1,200-frame top-down session (rotating while moving, around rotated walls) on both versions:
The one remaining platformer frame is the same frame in both versions: walking left off the top of the ramp, in the x-move and ramp code this PR doesn't touch. The next update's starting-overlap check frees it in both. I've left that for a separate change.
Performance
Old and new alternated, 3 rounds, µs:
Tests
New file
tests/unit/physics_engine/test_physics_engine_push_out.py:test_land_exactly_on_floor(six fall speeds): the bottom ends exactly at the floor's top, not colliding, andcan_jump()is true.test_land_on_ramp(four slopes, including negative): not colliding, and within 0.01 px of the surface.test_ceiling_bump_exact(four speeds).test_rotate_while_moving_sideways: both engines, three speeds. Never inside the wall over 40 updates.test_start_overlapping_smallest_move: both engines. The sprite moves exactlyget_collision_info's depth.test_start_overlapping_wedged(both engines) andtest_start_overlapping_concave_wall: the fallback still frees the sprite.test_separation_distance: 3,000 random pairs and directions. Movingdistance + 1e-6separates the sprites anddistance - 1e-6doesn't. A separate 40,000-pair check, not committed, found no mismatches.On the old engine, 20 of these fail: the landing gap, ramp and ceiling precision, rotate-while-moving, and smallest move. The 5 that pass on both are the wedged, concave, and moving-away cases, which check the new code isn't worse.
Deliberate bugs are caught. I removed each safeguard in turn:
Full suite on pyglet 3.0.dev11: 1461 passed, including the one-frame runs of every example (the platform tutorial uses this engine). The 3 failures are the render tests that only fail on my machine. The physics and collision tests also pass with Linux's
sin(π/4)value simulated. Ruff is clean on the changed files, and mypy reports no errors in them.🤖 Generated with Claude Code