Skip to content

Move sprites out of walls exactly in the physics engines - #2912

Merged
pvcraven merged 1 commit into
developmentfrom
perf/physics-push-out
Oct 5, 2026
Merged

pvcraven merged 1 commit into
developmentfrom
perf/physics-push-out

Conversation

@pvcraven

@pvcraven pvcraven commented Oct 5, 2026

Copy link
Copy Markdown
Member

Summary

PR 3 of the push-out plan (after #2910 and #2911). PhysicsEngineSimple and PhysicsEnginePlatformer now 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

Situation Before After
Sprite starts the update inside a wall "Wiggle" search: try 8 points at 1, 2, 4, 8… px Move out of the deepest wall along the smallest distance (get_collision_info_with_list), up to 8 times for several walls
Rotating into a wall Wiggle search; undo the rotation if it moved too far Same smallest-distance move; undo the rotation if it moved too far
Jumping into a ceiling Step down 1 px at a time until free Move exactly down out of the ceiling
Landing on a floor or ramp Step up 0.25 px at a time until free (per sprite hit) Move exactly up out of each sprite hit
  • Vertical moves: these use a new private helper, collision._get_separation_distance(a, b, dx, dy), which gives the exact distance to move a along a fixed direction until it stops colliding with b. It uses the same separating axis test, with the cached edge normals.
  • Fallback: if the exact method doesn't free the sprite (for example wedged between two walls, or with a concave detailed hit box), the old search runs from the original position, so behavior is never worse than before.
  • Unchanged: the x-axis binary search and the ramp-climbing logic in the platformer.

Rounding

The engines already round center_y to 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_sprite records original_x before rotating, and the x move then sets center_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

  • Exact resting positions: sprites stop exactly at floors, ramps and ceilings, within the 0.01 px rounding. Before, they could stop up to 0.25 px above a floor or ramp, or up to 1 px below a ceiling.
  • Rotation moves: rotating into a wall moves the sprite the exact distance needed, e.g. 0.0865 px for a 10×10 sprite rotated 1°, instead of a whole number of pixels (1 px, or 4 px at 45°).
  • Pinned tests: six assertions in test_physics_engine2.py pinned the old wiggle results. I updated them to the exact values (computed from the geometry, e.g. 5√2 − 5 for 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:

Platformer Top-down
Frames ending inside a wall, before → after 1 → 1 3 → 0
Largest vertical difference 0.45 px 7.3 px (after the rotation fix diverges)
Final position, before → after (1970.00, 194.75) → (1969.91, 194.60) (710.00, 545.00) → (709.47, 544.80)

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:

Case Before After
Hard landing (18 px/frame into a floor) 226–233 42–43
Ceiling bump 102–105 35–40
Standing on the ground (gravity pulls into it every frame) 49–52 43–48
Scripted platformer session, per frame 56.5–57.4 50.3–53.5
Scripted top-down session, per frame 81.3–84.9 51.5–52.4

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, and can_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 exactly get_collision_info's depth.
  • test_start_overlapping_wedged (both engines) and test_start_overlapping_concave_wall: the fallback still frees the sprite.
  • test_separation_distance: 3,000 random pairs and directions. Moving distance + 1e-6 separates the sprites and distance - 1e-6 doesn'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:

  • measuring the x move from before rotating: 4 failures,
  • dropping the rounding guard: 6 failures,
  • dropping the starting-overlap fallback: 2 failures.

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

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>
@pvcraven
pvcraven merged commit fde54bd into development Oct 5, 2026
7 checks passed
@pvcraven
pvcraven deleted the perf/physics-push-out branch October 5, 2026 18:49
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