diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d42aaa89..28ac23a7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. ## Unreleased ### Fixes +- Fixed a player in `PhysicsEnginePlatformer` sliding along when standing across two moving platforms. It moved by both platforms' speeds added together, so it slid until it stood on only one. It now moves with the platform it stands on the most, and isn't moved by a platform it only touches the edge of while standing mostly on something still ([#2782](https://github.com/pythonarcade/arcade/issues/2782)). - Fixed the PyInstaller hook bundling Arcade's version file at `arcade/VERSION/VERSION`. Frozen apps printed an error on startup and reported `arcade.VERSION` as `0.0.0`, and builds using `collect_all("arcade")` failed because the hook and `collect_all` put a folder and a file at the same path. The version file is now `arcade/_VERSION`, so it can't be confused with the `arcade.version` module ([#2837](https://github.com/pythonarcade/arcade/issues/2837)). - Fixed drawing into a texture atlas losing new drawing when the atlas grows during `render_into()`. Nested rendering and exception exits now restore the previous camera and render region. - Fixed several `arcade.create_text_sprite()` bugs: diff --git a/arcade/physics_engines.py b/arcade/physics_engines.py index 720296609..46f80d330 100644 --- a/arcade/physics_engines.py +++ b/arcade/physics_engines.py @@ -324,14 +324,21 @@ def _move_sprite( # print(f"Spot X ({self.player_sprite.center_x}, {self.player_sprite.center_y})" # f" {self.player_sprite.change_y}") elif moving_sprite.change_y < 0: - # Reset number of jumps + # Move straight up out of what we landed on, which also works + # for ramps for item in hit_list_x: - # Move straight up, which also works for ramps _move_out_of_sprite(moving_sprite, item, 1.0, 0.25) - # NOTE: Not all sprites have velocity - if getattr(item, "change_x", 0.0) != 0: - moving_sprite.center_x += item.change_x # type: ignore + # Move with what the sprite stands on the most, or if that's a + # tie, the highest. Moving with each moving platform it touches + # would add their speeds together. + def footing(item: SpriteType) -> tuple[float, float]: + overlap = min(moving_sprite.right, item.right) - max(moving_sprite.left, item.left) + return overlap, item.top + + # NOTE: Not all sprites have velocity + standing_on = max(hit_list_x, key=footing) + moving_sprite.center_x += getattr(standing_on, "change_x", 0.0) # print(f"Spot Y ({self.player_sprite.center_x}, {self.player_sprite.center_y})") else: diff --git a/tests/unit/physics_engine/test_physics_engine_moving_platforms.py b/tests/unit/physics_engine/test_physics_engine_moving_platforms.py new file mode 100644 index 000000000..ede5f72d8 --- /dev/null +++ b/tests/unit/physics_engine/test_physics_engine_moving_platforms.py @@ -0,0 +1,72 @@ +""" +A player standing on moving platforms in PhysicsEnginePlatformer moves with +them. Standing across two platforms used to move it by both their speeds +added together, so it slid along until it stood on only one (#2782). +""" + +import pytest + +import arcade + +PLATFORM_WIDTH = 64 +SIZE = 32 + + +def _platforms(*speeds): + """A row of touching platforms with their tops at y = 32.""" + platforms = arcade.SpriteList() + for i, speed in enumerate(speeds): + platform = arcade.SpriteSolidColor( + PLATFORM_WIDTH, 32, center_x=100 + i * PLATFORM_WIDTH, center_y=16 + ) + platform.change_x = speed + platforms.append(platform) + return platforms + + +def _run(player_x, *speeds, frames=30): + platforms = _platforms(*speeds) + player = arcade.SpriteSolidColor(SIZE, SIZE, center_x=player_x, center_y=48) + engine = arcade.PhysicsEnginePlatformer(player, platforms=platforms, gravity_constant=1) + for _ in range(frames): + engine.update() + return player, platforms + + +@pytest.mark.parametrize("speed", [2, -3]) +def test_one_platform(speed): + player, platforms = _run(100, speed) + assert player.center_x == pytest.approx(100 + 30 * speed) + assert player.bottom == pytest.approx(platforms[0].top) + + +@pytest.mark.parametrize("speed", [2, -3]) +@pytest.mark.parametrize("count", [2, 4]) +def test_across_platforms_moving_together(speed, count): + # Centered on the seam between the first two platforms + player, platforms = _run(132, *[speed] * count) + assert player.center_x == pytest.approx(132 + 30 * speed) + assert player.bottom == pytest.approx(platforms[0].top) + + +@pytest.mark.parametrize( + ("player_x", "expected_change"), + [ + # Mostly on the left platform, which moves right + (120, 2), + # Mostly on the right platform, which moves left + (144, -2), + ], +) +def test_across_platforms_moving_apart(player_x, expected_change): + player, _ = _run(player_x, 2, -2, frames=1) + assert player.center_x == pytest.approx(player_x + expected_change) + + +def test_moving_platform_next_to_still_one(): + # Mostly on a still platform, a little on a moving one + player, _ = _run(120, 0, 2, frames=1) + assert player.center_x == pytest.approx(120) + # Mostly on the moving one + player, _ = _run(144, 0, 2, frames=1) + assert player.center_x == pytest.approx(146)