Skip to content

Introduce SpriteSequence, a covariant supertype of SpriteList. #2647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions arcade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ def configure_logging(level: int | None = None):
from .sprite import PyMunk
from .sprite import PymunkMixin
from .sprite import SpriteType
from .sprite import SpriteType_co
from .sprite import Sprite
from .sprite import BasicSprite

Expand All @@ -176,6 +177,7 @@ def configure_logging(level: int | None = None):
from .sprite import SpriteSolidColor

from .sprite_list import SpriteList
from .sprite_list import SpriteSequence
from .sprite_list import check_for_collision
from .sprite_list import check_for_collision_with_list
from .sprite_list import check_for_collision_with_lists
Expand Down Expand Up @@ -283,9 +285,11 @@ def configure_logging(level: int | None = None):
"BasicSprite",
"Sprite",
"SpriteType",
"SpriteType_co",
"PymunkMixin",
"SpriteCircle",
"SpriteList",
"SpriteSequence",
"SpriteSolidColor",
"Text",
"Texture",
Expand Down
6 changes: 3 additions & 3 deletions arcade/future/input/input_manager_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Player(arcade.Sprite):
def __init__(
self,
texture,
walls: arcade.SpriteList,
walls: arcade.SpriteSequence[arcade.BasicSprite],
input_manager_template: InputManager,
controller: pyglet.input.Controller | None = None,
center_x: float = 0.0,
Expand Down Expand Up @@ -76,11 +76,11 @@ def __init__(
}

self.players: list[Player | None] = []
self.player_list = arcade.SpriteList()
self.player_list: arcade.SpriteList[Player] = arcade.SpriteList()
self.device_labels_batch = pyglet.graphics.Batch()
self.player_device_labels: list[arcade.Text | None] = []

self.wall_list = arcade.SpriteList(use_spatial_hash=True)
self.wall_list: arcade.SpriteList[arcade.Sprite] = arcade.SpriteList(use_spatial_hash=True)

for x in range(0, self.width + 64, 64):
wall = arcade.Sprite(":resources:images/tiles/grassMid.png", scale=0.5)
Expand Down
2 changes: 1 addition & 1 deletion arcade/future/light/light_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, width, height, title):
super().__init__(width, height, title)
self.background = arcade.load_texture(":resources:images/backgrounds/abstract_1.jpg")

self.torch_list = arcade.SpriteList()
self.torch_list: arcade.SpriteList[arcade.Sprite] = arcade.SpriteList()
self.torch_list.extend(
[
arcade.Sprite(
Expand Down
6 changes: 3 additions & 3 deletions arcade/particles/emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import annotations

from typing import Callable, cast
from typing import Callable

import arcade
from arcade import Vec2
Expand Down Expand Up @@ -151,7 +151,7 @@ def __init__(
self.particle_factory = particle_factory
self._emit_done_cb = emit_done_cb
self._reap_cb = reap_cb
self._particles: arcade.SpriteList = arcade.SpriteList(use_spatial_hash=False)
self._particles: arcade.SpriteList[Particle] = arcade.SpriteList(use_spatial_hash=False)

def _emit(self):
"""
Expand Down Expand Up @@ -189,7 +189,7 @@ def update(self, delta_time: float = 1 / 60):
for _ in range(emit_count):
self._emit()
self._particles.update(delta_time)
particles_to_reap = [p for p in self._particles if cast(Particle, p).can_reap()]
particles_to_reap = [p for p in self._particles if p.can_reap()]
for dead_particle in particles_to_reap:
dead_particle.kill()

Expand Down
16 changes: 12 additions & 4 deletions arcade/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@

import math

from arcade import Sprite, SpriteList, check_for_collision_with_list, get_sprites_at_point
from arcade import (
BasicSprite,
Sprite,
SpriteSequence,
check_for_collision_with_list,
get_sprites_at_point,
)
from arcade.math import get_distance, lerp_2d
from arcade.types import Point2

__all__ = ["AStarBarrierList", "astar_calculate_path", "has_line_of_sight"]


def _spot_is_blocked(position: Point2, moving_sprite: Sprite, blocking_sprites: SpriteList) -> bool:
def _spot_is_blocked(
position: Point2, moving_sprite: Sprite, blocking_sprites: SpriteSequence[BasicSprite]
) -> bool:
"""
Return if position is blocked

Expand Down Expand Up @@ -275,7 +283,7 @@ class AStarBarrierList:
def __init__(
self,
moving_sprite: Sprite,
blocking_sprites: SpriteList,
blocking_sprites: SpriteSequence[BasicSprite],
grid_size: int,
left: int,
right: int,
Expand Down Expand Up @@ -372,7 +380,7 @@ def astar_calculate_path(
def has_line_of_sight(
observer: Point2,
target: Point2,
walls: SpriteList,
walls: SpriteSequence[BasicSprite],
max_distance: float = float("inf"),
check_resolution: int = 2,
) -> bool:
Expand Down
58 changes: 36 additions & 22 deletions arcade/physics_engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from arcade import (
BasicSprite,
Sprite,
SpriteList,
SpriteSequence,
SpriteType,
check_for_collision,
check_for_collision_with_lists,
Expand All @@ -20,7 +20,7 @@
from arcade.utils import Chain, copy_dunders_unimplemented


def _wiggle_until_free(colliding: Sprite, walls: Iterable[SpriteList]) -> None:
def _wiggle_until_free(colliding: Sprite, walls: Iterable[SpriteSequence[BasicSprite]]) -> None:
"""Kludge to 'guess' a colliding sprite out of a collision.

It works by iterating over increasing wiggle sizes of 8 points
Expand Down Expand Up @@ -80,7 +80,7 @@ def _wiggle_until_free(colliding: Sprite, walls: Iterable[SpriteList]) -> None:


def _move_sprite(
moving_sprite: Sprite, can_collide: Iterable[SpriteList[SpriteType]], ramp_up: bool
moving_sprite: Sprite, can_collide: Iterable[SpriteSequence[SpriteType]], ramp_up: bool
) -> list[SpriteType]:
"""Update a sprite's angle and position, returning a list of collisions.

Expand Down Expand Up @@ -273,11 +273,14 @@ def _move_sprite(
return complete_hit_list


def _add_to_list(dest: list[SpriteList], source: SpriteList | Iterable[SpriteList] | None) -> None:
"""Helper function to add a SpriteList or list of SpriteLists to a list."""
def _add_to_list(
dest: list[SpriteSequence[SpriteType]],
source: SpriteSequence[SpriteType] | Iterable[SpriteSequence[SpriteType]] | None,
) -> None:
"""Helper function to add a SpriteSequence or list of SpriteSequences to a list."""
if not source:
return
elif isinstance(source, SpriteList):
elif isinstance(source, SpriteSequence):
dest.append(source)
else:
dest.extend(source)
Expand Down Expand Up @@ -310,17 +313,17 @@ class PhysicsEngineSimple:
def __init__(
self,
player_sprite: Sprite,
walls: SpriteList | Iterable[SpriteList] | None = None,
walls: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
) -> None:
self.player_sprite: Sprite = player_sprite
"""The player-controlled :py:class:`.Sprite`."""
self._walls: list[SpriteList] = []
self._walls: list[SpriteSequence[BasicSprite]] = []

if walls:
_add_to_list(self._walls, walls)

@property
def walls(self) -> list[SpriteList]:
def walls(self) -> list[SpriteSequence[BasicSprite]]:
"""Which :py:class:`.SpriteList` instances block player movement.

.. important:: Avoid moving sprites in these lists!
Expand All @@ -334,7 +337,10 @@ def walls(self) -> list[SpriteList]:
return self._walls

@walls.setter
def walls(self, walls: SpriteList | Iterable[SpriteList] | None = None) -> None:
def walls(
self,
walls: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
) -> None:
if walls:
_add_to_list(self._walls, walls)
else:
Expand Down Expand Up @@ -429,17 +435,17 @@ class PhysicsEnginePlatformer:
def __init__(
self,
player_sprite: Sprite,
platforms: SpriteList | Iterable[SpriteList] | None = None,
platforms: SpriteSequence[Sprite] | Iterable[SpriteSequence[Sprite]] | None = None,
gravity_constant: float = 0.5,
ladders: SpriteList | Iterable[SpriteList] | None = None,
walls: SpriteList | Iterable[SpriteList] | None = None,
ladders: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
walls: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
) -> None:
if not isinstance(player_sprite, Sprite):
raise TypeError("player_sprite must be a Sprite, not a basic_sprite!")

self._ladders: list[SpriteList] = []
self._platforms: list[SpriteList] = []
self._walls: list[SpriteList] = []
self._ladders: list[SpriteSequence[BasicSprite]] = []
self._platforms: list[SpriteSequence[Sprite]] = []
self._walls: list[SpriteSequence[BasicSprite]] = []
self._all_obstacles = Chain(self._walls, self._platforms)

_add_to_list(self._ladders, ladders)
Expand Down Expand Up @@ -517,7 +523,7 @@ def __init__(
# TODO: figure out what do do with 15_ladders_moving_platforms.py
# It's no longer used by any example or tutorial file
@property
def ladders(self) -> list[SpriteList]:
def ladders(self) -> list[SpriteSequence[BasicSprite]]:
"""Ladders turn off gravity while touched by the player.

This means that whenever the :py:attr:`player_sprite` collides
Expand All @@ -533,7 +539,10 @@ def ladders(self) -> list[SpriteList]:
return self._ladders

@ladders.setter
def ladders(self, ladders: SpriteList | Iterable[SpriteList] | None = None) -> None:
def ladders(
self,
ladders: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
) -> None:
if ladders:
_add_to_list(self._ladders, ladders)
else:
Expand All @@ -544,7 +553,7 @@ def ladders(self) -> None:
self._ladders.clear()

@property
def platforms(self) -> list[SpriteList]:
def platforms(self) -> list[SpriteSequence[Sprite]]:
""":py:class:`~arcade.sprite_list.sprite_list.SpriteList` instances containing platforms.

.. important:: For best performance, put non-moving terrain in
Expand Down Expand Up @@ -575,7 +584,9 @@ def platforms(self) -> list[SpriteList]:
return self._platforms

@platforms.setter
def platforms(self, platforms: SpriteList | Iterable[SpriteList] | None = None) -> None:
def platforms(
self, platforms: SpriteSequence[Sprite] | Iterable[SpriteSequence[Sprite]] | None = None
) -> None:
if platforms:
_add_to_list(self._platforms, platforms)
else:
Expand All @@ -586,7 +597,7 @@ def platforms(self) -> None:
self._platforms.clear()

@property
def walls(self) -> list[SpriteList]:
def walls(self) -> list[SpriteSequence[BasicSprite]]:
"""Exposes the :py:class:`SpriteList` instances use as terrain.

.. important:: For best performance, only add non-moving sprites!
Expand All @@ -611,7 +622,10 @@ def walls(self) -> list[SpriteList]:
return self._walls

@walls.setter
def walls(self, walls: SpriteList | Iterable[SpriteList] | None = None) -> None:
def walls(
self,
walls: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
) -> None:
if walls:
_add_to_list(self._walls, walls)
else:
Expand Down
3 changes: 2 additions & 1 deletion arcade/sprite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from arcade.texture import Texture
from arcade.resources import resolve
from .base import BasicSprite, SpriteType
from .base import BasicSprite, SpriteType, SpriteType_co
from .sprite import Sprite
from .mixins import PymunkMixin, PyMunk
from .animated import (
Expand Down Expand Up @@ -69,6 +69,7 @@ def load_animated_gif(resource_name: str | Path) -> TextureAnimationSprite:

__all__ = [
"SpriteType",
"SpriteType_co",
"BasicSprite",
"Sprite",
"PyMunk",
Expand Down
21 changes: 17 additions & 4 deletions arcade/sprite/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# Type from sprite that can be any BasicSprite or any subclass of BasicSprite
SpriteType = TypeVar("SpriteType", bound="BasicSprite")

# Same as SpriteType, for covariant type parameters
SpriteType_co = TypeVar("SpriteType_co", bound="BasicSprite", covariant=True)


@copy_dunders_unimplemented # See https://github.com/pythonarcade/arcade/issues/2074
class BasicSprite:
Expand Down Expand Up @@ -70,7 +73,15 @@ def __init__(
self._height = height * self._scale[1]
self._visible = bool(visible)
self._color: Color = WHITE
self.sprite_lists: list["SpriteList"] = []

# In a more powerful type system, this would be typed as
# list[SpriteList[? super Self]]
# i.e., a list of SpriteList's with varying type arguments, but where
# each of those type arguments is known to be a supertype of Self.
# All changes to this list should go through the pair of methods
# register_sprite_list, _unregister_sprite_list.
# They ensure that the above typing invariant is preserved.
self.sprite_lists: list["SpriteList[Any]"] = []
"""The sprite lists this sprite is a member of"""

# Core properties we don't use, but spritelist expects it
Expand Down Expand Up @@ -747,21 +758,23 @@ def update_spatial_hash(self) -> None:
if sprite_list.spatial_hash is not None:
sprite_list.spatial_hash.move(self)

def register_sprite_list(self, new_list: SpriteList) -> None:
def register_sprite_list(self: SpriteType, new_list: SpriteList[SpriteType]) -> None:
"""
Register this sprite as belonging to a list.

We will automatically remove ourselves from the list when kill() is called.
"""
self.sprite_lists.append(new_list)

def _unregister_sprite_list(self: SpriteType, new_list: SpriteList[SpriteType]) -> None:
"""Unregister this sprite as belonging to a list."""
self.sprite_lists.remove(new_list)

def remove_from_sprite_lists(self) -> None:
"""Remove the sprite from all sprite lists."""
while len(self.sprite_lists) > 0:
self.sprite_lists[0].remove(self)

self.sprite_lists.clear()

# ----- Drawing Methods -----

def draw_hit_box(self, color: RGBOrA255 = BLACK, line_thickness: float = 2.0) -> None:
Expand Down
7 changes: 1 addition & 6 deletions arcade/sprite/sprite.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import math
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import Any

import arcade
from arcade import Texture
Expand All @@ -11,10 +11,6 @@
from .base import BasicSprite
from .mixins import PymunkMixin

if TYPE_CHECKING: # handle import cycle caused by type hinting
from arcade.sprite_list import SpriteList


__all__ = ["Sprite"]


Expand Down Expand Up @@ -141,7 +137,6 @@ def __init__(
self.physics_engines: list[Any] = []
"""List of physics engines that have registered this sprite."""

self._sprite_list: SpriteList | None = None
# Debug properties
self.guid: str | None = None
"""A unique id for debugging purposes."""
Expand Down
Loading
Loading