From 3e24ae97d2d7bd740037f6493d41c700f552f447 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 1 Oct 2026 16:46:59 -0500 Subject: [PATCH] Add get_collision_info_with_list and sprite_push_out example get_collision_info_with_list(sprite, sprite_list, method=AUTO) returns a (sprite, CollisionInfo) pair for each colliding sprite in the list, deepest overlap first. It finds the same sprites as check_for_collision_with_list and each CollisionInfo matches get_collision_info. Split get_collision_info into the public function and an internal one without the argument checks, for the list loop. Add the sprite_push_out example: the player moves freely and is pushed out of the deepest overlapping wall after each update, sliding along rotated walls. Add its docs page, screenshot, and an entry in the example index under Player Movement. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + arcade/__init__.py | 2 + arcade/examples/sprite_push_out.py | 133 ++++++++++++++++++++ arcade/sprite_list/__init__.py | 2 + arcade/sprite_list/collision.py | 63 ++++++++++ doc/example_code/images/sprite_push_out.png | Bin 0 -> 44034 bytes doc/example_code/index.rst | 6 + doc/example_code/sprite_push_out.rst | 20 +++ tests/unit/sprite/test_sprite_collision.py | 48 +++++++ 9 files changed, 275 insertions(+) create mode 100644 arcade/examples/sprite_push_out.py create mode 100644 doc/example_code/images/sprite_push_out.png create mode 100644 doc/example_code/sprite_push_out.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f03abf81..05a72d107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. - Added `arcade.has_collision_with_list()` and `arcade.has_collision_with_lists()`, which return `True` as soon as they find a collision. They're faster than checking whether `check_for_collision_with_list()` returns an empty list, by about 18x when many sprites overlap. - Added `arcade.check_for_collision_between_lists(list_a, list_b)`, which returns every colliding `(sprite_a, sprite_b)` pair between two lists, such as bullets and enemies. Passing the same list twice returns each pair once. - Added `arcade.get_collision_info(sprite1, sprite2)`, which 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, or `None` if they don't collide. For example, `player.position += info.normal * info.depth` pushes a player out of a wall. Correct for convex hit boxes. +- Added `arcade.get_collision_info_with_list(sprite, sprite_list)`, which returns a `(sprite, CollisionInfo)` pair for each sprite in the list that `sprite` collides with, deepest overlap first. Added the `sprite_push_out` example, which uses it to push a player out of walls, sliding along rotated ones. ### Misc Changes - Sped up several `SpriteList` operations: `swap()` no longer searches the draw order (about 1000x faster at the end of a 10,000 sprite list), `insert()` and item assignment check membership with a dictionary instead of scanning the list, and drawing uploads only the buffer slots in use instead of the whole capacity (moving one sprite and drawing is 1.6-2.3x faster). diff --git a/arcade/__init__.py b/arcade/__init__.py index b85a49048..3a070143a 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -187,6 +187,7 @@ def configure_logging(level: int | None = None): from .sprite_list import CollisionMethod from .sprite_list import check_for_collision from .sprite_list import get_collision_info +from .sprite_list import get_collision_info_with_list from .sprite_list import check_for_collision_with_list from .sprite_list import check_for_collision_with_lists from .sprite_list import check_for_collision_between_lists @@ -335,6 +336,7 @@ def configure_logging(level: int | None = None): "astar_calculate_path", "check_for_collision", "get_collision_info", + "get_collision_info_with_list", "check_for_collision_with_list", "check_for_collision_with_lists", "check_for_collision_between_lists", diff --git a/arcade/examples/sprite_push_out.py b/arcade/examples/sprite_push_out.py new file mode 100644 index 000000000..637c3bcbe --- /dev/null +++ b/arcade/examples/sprite_push_out.py @@ -0,0 +1,133 @@ +""" +Push a Sprite Out of Walls + +Move the player with the arrow keys. It isn't stopped by a physics engine; +instead, after each move, any walls it overlaps push it back out using +arcade.get_collision_info_with_list(). Pushing out along the smallest +overlap lets the player slide along walls, including the rotated ones. + +Artwork from https://kenney.nl + +If Python and Arcade are installed, this example can be run from the command line with: +python -m arcade.examples.sprite_push_out +""" + +import arcade + +SPRITE_SCALING = 0.5 + +WINDOW_WIDTH = 1280 +WINDOW_HEIGHT = 720 +WINDOW_TITLE = "Push a Sprite Out of Walls Example" + +MOVEMENT_SPEED = 5 + +# A sprite can overlap more than one wall. Pushing it out of one can push +# it into another, so try a few times. +MAX_PUSHES = 4 + + +class GameView(arcade.View): + """Main application class.""" + + def __init__(self): + super().__init__() + + self.player_sprite = arcade.Sprite( + ":resources:images/animated_characters/female_person/femalePerson_idle.png", + scale=SPRITE_SCALING, + ) + self.player_list = arcade.SpriteList() + self.player_list.append(self.player_sprite) + + self.wall_list = arcade.SpriteList() + + self.background_color = arcade.color.AMAZON + + def setup(self): + """Set up the game and initialize the variables.""" + self.player_sprite.position = 200, 300 + + self.wall_list.clear() + wall_texture = ":resources:images/tiles/boxCrate_double.png" + + # The walls of a room + for x in range(96, 1200, 64): + self.wall_list.append( + arcade.Sprite(wall_texture, scale=SPRITE_SCALING, center_x=x, center_y=96) + ) + self.wall_list.append( + arcade.Sprite(wall_texture, scale=SPRITE_SCALING, center_x=x, center_y=608) + ) + for y in range(160, 608, 64): + self.wall_list.append( + arcade.Sprite(wall_texture, scale=SPRITE_SCALING, center_x=96, center_y=y) + ) + self.wall_list.append( + arcade.Sprite(wall_texture, scale=SPRITE_SCALING, center_x=1184, center_y=y) + ) + + # Rotated walls the player slides along instead of stopping at + for x, y, angle in ((450, 380, 45), (800, 260, -30), (950, 420, 15)): + self.wall_list.append( + arcade.Sprite( + wall_texture, + scale=SPRITE_SCALING * 2, + center_x=x, + center_y=y, + angle=angle, + ) + ) + + def on_draw(self): + """Render the screen.""" + self.clear() + self.wall_list.draw() + self.player_list.draw() + + def push_player_out_of_walls(self): + """Move the player out of any walls it overlaps.""" + for _ in range(MAX_PUSHES): + # Results are sorted with the deepest overlap first + hits = arcade.get_collision_info_with_list(self.player_sprite, self.wall_list) + if not hits: + return + _wall, info = hits[0] + # Move the smallest distance that separates the player from this wall + self.player_sprite.position += info.normal * info.depth + + def on_update(self, delta_time): + """Movement and game logic""" + self.player_sprite.update() + self.push_player_out_of_walls() + + def on_key_press(self, key, modifiers): + """Called whenever a key is pressed.""" + if key == arcade.key.UP: + self.player_sprite.change_y = MOVEMENT_SPEED + elif key == arcade.key.DOWN: + self.player_sprite.change_y = -MOVEMENT_SPEED + elif key == arcade.key.LEFT: + self.player_sprite.change_x = -MOVEMENT_SPEED + elif key == arcade.key.RIGHT: + self.player_sprite.change_x = MOVEMENT_SPEED + + def on_key_release(self, key, modifiers): + """Called when the user releases a key.""" + if key in (arcade.key.UP, arcade.key.DOWN): + self.player_sprite.change_y = 0 + elif key in (arcade.key.LEFT, arcade.key.RIGHT): + self.player_sprite.change_x = 0 + + +def main(): + """Main function""" + window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) + game = GameView() + game.setup() + window.show_view(game) + arcade.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/sprite_list/__init__.py b/arcade/sprite_list/__init__.py index 07e3fe24e..4ff968007 100644 --- a/arcade/sprite_list/__init__.py +++ b/arcade/sprite_list/__init__.py @@ -7,6 +7,7 @@ get_closest_sprite, check_for_collision, get_collision_info, + get_collision_info_with_list, check_for_collision_with_list, check_for_collision_with_lists, check_for_collision_between_lists, @@ -28,6 +29,7 @@ "get_closest_sprite", "check_for_collision", "get_collision_info", + "get_collision_info_with_list", "check_for_collision_with_list", "check_for_collision_with_lists", "check_for_collision_between_lists", diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index 8bb1a765b..7897cc04a 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -210,6 +210,11 @@ def get_collision_info(sprite1: BasicSprite, sprite2: BasicSprite) -> CollisionI elif not isinstance(sprite2, BasicSprite): raise TypeError("Parameter 2 is not an instance of a Sprite class.") + return _get_collision_info(sprite1, sprite2) + + +def _get_collision_info(sprite1: BasicSprite, sprite2: BasicSprite) -> CollisionInfo | None: + """:py:func:`get_collision_info` without the argument type checks.""" hit_box1 = sprite1._hit_box hit_box2 = sprite2._hit_box @@ -549,6 +554,64 @@ def check_for_collision_between_lists( return pairs +def get_collision_info_with_list( + sprite: BasicSprite, + sprite_list: SpriteSequence[SpriteType], + method: CollisionMethod | int = CollisionMethod.AUTO, +) -> list[tuple[SpriteType, CollisionInfo]]: + """ + Find the sprites in a list that a sprite collides with, and how to separate them. + + This works like :py:func:`check_for_collision_with_list`, but also + returns a :py:class:`CollisionInfo` for each colliding sprite, as from + :py:func:`get_collision_info`. The results are sorted deepest overlap + first, which is usually the one to resolve first:: + + for wall, info in arcade.get_collision_info_with_list(player, walls): + # Each move changes the remaining overlaps, so check again + info = arcade.get_collision_info(player, wall) + if info: + player.position += info.normal * info.depth + + Each :py:class:`CollisionInfo` is how to separate ``sprite`` from that + one sprite. Moving ``sprite`` to resolve one overlap changes the others, + so they don't add up to a move that resolves them all. + + Args: + sprite: + The sprite to separate + sprite_list: + SpriteList to check against + method: + How to find the sprites to check. See :py:class:`CollisionMethod`. + Defaults to :py:attr:`CollisionMethod.AUTO`. + + Returns: + A list of ``(colliding_sprite, CollisionInfo)`` tuples, deepest first, + or an empty list. + """ + if __debug__: + if not isinstance(sprite, BasicSprite): + raise TypeError( + f"Parameter 1 is not an instance of the Sprite class, " + f"it is an instance of {type(sprite)}." + ) + if not isinstance(sprite_list, SpriteSequence): + raise TypeError(f"Parameter 2 is a {type(sprite_list)} instead of expected SpriteList.") + + results: list[tuple[SpriteType, CollisionInfo]] = [] + for sprite2 in _get_sprites_to_check(sprite, sprite_list, method): + if sprite is not sprite2: + info = _get_collision_info(sprite, sprite2) + if info is not None: + results.append((sprite2, info)) + + # Deepest first. The sort is stable, so equal depths keep the order found. + if len(results) > 1: + results.sort(key=lambda result: result[1].depth, reverse=True) + return results + + def check_for_collision_with_lists( sprite: BasicSprite, sprite_lists: Iterable[SpriteSequence[SpriteType]], diff --git a/doc/example_code/images/sprite_push_out.png b/doc/example_code/images/sprite_push_out.png new file mode 100644 index 0000000000000000000000000000000000000000..784a8795ab4979bbc48127de9b9fc48c4aea4d72 GIT binary patch literal 44034 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 ao^vp#ZqC{B7Y}3BrLT|tQu@WNoc{skzvoB* literal 0 HcmV?d00001 diff --git a/doc/example_code/index.rst b/doc/example_code/index.rst index 7da70d943..867c875ac 100644 --- a/doc/example_code/index.rst +++ b/doc/example_code/index.rst @@ -182,6 +182,12 @@ Player Movement :ref:`sprite_rotate_around_tank` +.. figure:: images/thumbs/sprite_push_out.png + :figwidth: 170px + :target: sprite_push_out.html + + :ref:`sprite_push_out` + diff --git a/doc/example_code/sprite_push_out.rst b/doc/example_code/sprite_push_out.rst new file mode 100644 index 000000000..9f2dbfa0b --- /dev/null +++ b/doc/example_code/sprite_push_out.rst @@ -0,0 +1,20 @@ +:orphan: + +.. _sprite_push_out: + +Push a Sprite Out of Walls +========================== + +.. image:: images/sprite_push_out.png + :width: 600px + :align: center + :alt: Screenshot of a player sprite being pushed out of rotated walls + +Instead of a physics engine, this example moves the player freely and then +uses :py:func:`arcade.get_collision_info_with_list` to push it back out of +any walls it overlaps. Because the push is along the smallest overlap, the +player slides along walls, including rotated ones. + +.. literalinclude:: ../../arcade/examples/sprite_push_out.py + :caption: sprite_push_out.py + :linenos: diff --git a/tests/unit/sprite/test_sprite_collision.py b/tests/unit/sprite/test_sprite_collision.py index bbda724b9..5cbb4cb55 100644 --- a/tests/unit/sprite/test_sprite_collision.py +++ b/tests/unit/sprite/test_sprite_collision.py @@ -674,6 +674,54 @@ def test_get_collision_info_separates(window): assert checked > 300 +@pytest.mark.parametrize("method", list(arcade.CollisionMethod)) +@pytest.mark.parametrize("spatial", [False, True]) +def test_get_collision_info_with_list(window, method, spatial): + player = arcade.SpriteSolidColor(10, 10) + walls = arcade.SpriteList(use_spatial_hash=spatial) + shallow = arcade.SpriteSolidColor(10, 10, center_x=9) # 1 pixel overlap + deep = arcade.SpriteSolidColor(10, 10, center_y=-6) # 4 pixel overlap + touching = arcade.SpriteSolidColor(10, 10, center_x=-10) + far = arcade.SpriteSolidColor(10, 10, center_x=100) + walls.extend([shallow, deep, touching, far]) + # A sprite never collides with itself + walls.append(player) + + results = arcade.get_collision_info_with_list(player, walls, method=method) + # Deepest first, each matching get_collision_info + assert [wall for wall, _ in results] == [deep, shallow] + for wall, info in results: + assert info == arcade.get_collision_info(player, wall) + assert results[0][1] == (Vec2(0.0, 1.0), 4.0) + assert results[1][1] == (Vec2(-1.0, 0.0), 1.0) + + # Same sprites as check_for_collision_with_list finds + hits = arcade.check_for_collision_with_list(player, walls, method=method) + assert {wall for wall, _ in results} == set(hits) + + assert arcade.get_collision_info_with_list(player, arcade.SpriteList(), method=method) == [] + + +def test_get_collision_info_with_list_equal_depths(window): + """Equal depths keep the order the sprites were found in""" + player = arcade.SpriteSolidColor(10, 10) + walls = arcade.SpriteList() + left = arcade.SpriteSolidColor(10, 10, center_x=-8) + right = arcade.SpriteSolidColor(10, 10, center_x=8) + walls.extend([left, right]) + results = arcade.get_collision_info_with_list(player, walls) + assert [wall for wall, _ in results] == [left, right] + assert [info.depth for _, info in results] == [2.0, 2.0] + + +def test_get_collision_info_with_list_type_errors(window): + sprite = arcade.SpriteSolidColor(10, 10) + with pytest.raises(TypeError): + arcade.get_collision_info_with_list("moo", arcade.SpriteList()) + with pytest.raises(TypeError): + arcade.get_collision_info_with_list(sprite, "moo") + + def test_check_for_collision_with_list(window): # TODO: Check that the right collision function is called internally a = arcade.SpriteSolidColor(50, 50, color=arcade.csscolor.RED)