From b54e49c245853e0e80f5de5254cde01ebde2bf5a Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 14 Mar 2023 22:22:08 -0400 Subject: [PATCH 01/10] Initial very messy work on hitboxes --- arcade/examples/platform_tutorial/17_views.py | 8 +- arcade/geometry/geometry_python.py | 36 ++-- arcade/hitbox/__init__.py | 3 +- arcade/hitbox/base.py | 146 +++++++++++++++- arcade/sprite/sprite.py | 164 ++++++------------ arcade/sprite_list/collision.py | 6 +- arcade/texture/texture.py | 24 +-- arcade/tilemap/tilemap.py | 4 +- 8 files changed, 240 insertions(+), 151 deletions(-) diff --git a/arcade/examples/platform_tutorial/17_views.py b/arcade/examples/platform_tutorial/17_views.py index 82ab7df64..64d11f3a3 100644 --- a/arcade/examples/platform_tutorial/17_views.py +++ b/arcade/examples/platform_tutorial/17_views.py @@ -100,7 +100,7 @@ def __init__(self, name_folder, name_file): # Hit box will be set based on the first image used. If you want to specify # a different hit box, you can do it like the code below. # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]]) - self.set_hit_box(self.texture.hit_box_points) + self.set_hit_box(self.texture.hit_box.create_adjustable(self.position, self.angle, self.scale_xy)) class Enemy(Entity): @@ -400,9 +400,9 @@ def on_draw(self): self.scene.draw() # Draw hit boxes. - # self.scene[LAYER_NAME_COINS].draw_hit_boxes(color=arcade.color.WHITE) - # self.scene[LAYER_NAME_ENEMIES].draw_hit_boxes(color=arcade.color.WHITE) - # self.scene[LAYER_NAME_PLAYER].draw_hit_boxes(color=arcade.color.WHITE) + self.scene[LAYER_NAME_COINS].draw_hit_boxes(color=arcade.color.WHITE) + self.scene[LAYER_NAME_ENEMIES].draw_hit_boxes(color=arcade.color.WHITE) + self.scene[LAYER_NAME_PLAYER].draw_hit_boxes(color=arcade.color.WHITE) # Activate the GUI camera before drawing GUI elements self.gui_camera.use() diff --git a/arcade/geometry/geometry_python.py b/arcade/geometry/geometry_python.py index f8ddf6141..6ae370dac 100644 --- a/arcade/geometry/geometry_python.py +++ b/arcade/geometry/geometry_python.py @@ -5,19 +5,22 @@ Point in polygon function from https://www.geeksforgeeks.org/how-to-check-if-a-given-point-lies-inside-a-polygon/ """ -from arcade.types import Point, PointList +from arcade.types import Point +from arcade.hitbox import HitBox -def are_polygons_intersecting(poly_a: PointList, poly_b: PointList) -> bool: +def are_polygons_intersecting(poly_a: HitBox, poly_b: HitBox) -> bool: """ Return True if two polygons intersect. - :param PointList poly_a: List of points that define the first polygon. - :param PointList poly_b: List of points that define the second polygon. + :param HitBox poly_a: List of points that define the first polygon. + :param HitBox poly_b: List of points that define the second polygon. :Returns: True or false depending if polygons intersect :rtype bool: """ - for polygon in (poly_a, poly_b): + points_a = poly_a.get_adjusted_points() + points_b = poly_b.get_adjusted_points() + for polygon in (points_a, points_b): for i1 in range(len(polygon)): i2 = (i1 + 1) % len(polygon) @@ -29,7 +32,7 @@ def are_polygons_intersecting(poly_a: PointList, poly_b: PointList) -> bool: min_a, max_a, min_b, max_b = (None,) * 4 - for poly in poly_a: + for poly in points_a: projected = normal[0] * poly[0] + normal[1] * poly[1] if min_a is None or projected < min_a: @@ -37,7 +40,7 @@ def are_polygons_intersecting(poly_a: PointList, poly_b: PointList) -> bool: if max_a is None or projected > max_a: max_a = projected - for poly in poly_b: + for poly in points_b: projected = normal[0] * poly[0] + normal[1] * poly[1] if min_b is None or projected < min_b: @@ -134,7 +137,7 @@ def are_lines_intersecting(p1: Point, q1: Point, p2: Point, q2: Point) -> bool: return False -def is_point_in_polygon(x: float, y: float, polygon: PointList) -> bool: +def is_point_in_polygon(x: float, y: float, polygon: HitBox) -> bool: """ Checks if a point is inside a polygon of three or more points. @@ -143,8 +146,9 @@ def is_point_in_polygon(x: float, y: float, polygon: PointList) -> bool: :param PointList polygon_point_list: List of points that define the polygon. :Returns: True or false depending if point is inside polygon """ + points = polygon.get_adjusted_points() p = x, y - n = len(polygon) + n = len(points) # There must be at least 3 vertices # in polygon @@ -164,23 +168,23 @@ def is_point_in_polygon(x: float, y: float, polygon: PointList) -> bool: while True: next_item = (i + 1) % n - if polygon[i][1] == p[1]: + if points[i][1] == p[1]: decrease += 1 # Check if the line segment from 'p' to # 'extreme' intersects with the line # segment from 'polygon[i]' to 'polygon[next]' - if (are_lines_intersecting(polygon[i], - polygon[next_item], + if (are_lines_intersecting(points[i], + points[next_item], p, extreme)): # If the point 'p' is collinear with line # segment 'i-next', then check if it lies # on segment. If it lies, return true, otherwise false - if get_triangle_orientation(polygon[i], p, - polygon[next_item]) == 0: + if get_triangle_orientation(points[i], p, + points[next_item]) == 0: return not is_point_in_box( - polygon[i], p, - polygon[next_item], + points[i], p, + points[next_item], ) count += 1 diff --git a/arcade/hitbox/__init__.py b/arcade/hitbox/__init__.py index 1430dcb44..be4c9b297 100644 --- a/arcade/hitbox/__init__.py +++ b/arcade/hitbox/__init__.py @@ -1,6 +1,6 @@ from PIL.Image import Image from arcade.types import PointList -from .base import HitBoxAlgorithm +from .base import HitBoxAlgorithm, HitBox, AdjustableHitBox from .bounding_box import BoundingHitBoxAlgorithm from .simple import SimpleHitBoxAlgorithm from .pymunk import PymunkHitBoxAlgorithm @@ -47,6 +47,7 @@ def calculate_hit_box_points_detailed( __all__ = [ "HitBoxAlgorithm", + "HitBox", "SimpleHitBoxAlgorithm", "PymunkHitBoxAlgorithm", "BoundingHitBoxAlgorithm", diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index 5afcec54e..6e9129edf 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -1,6 +1,11 @@ -from typing import Any +from __future__ import annotations + +from math import cos, radians, sin +from typing import Any, Tuple + from PIL.Image import Image -from arcade.types import PointList + +from arcade.types import Point, PointList class HitBoxAlgorithm: @@ -8,6 +13,7 @@ class HitBoxAlgorithm: Base class for hit box algorithms. Hit box algorithms are used to calculate the points that make up a hit box for a sprite. """ + #: The name of the algorithm name = "base" #: Should points for this algorithm be cached? @@ -27,3 +33,139 @@ def calculate(self, image: Image, **kwargs) -> PointList: def __call__(self, *args: Any, **kwds: Any) -> "HitBoxAlgorithm": return self.__class__(*args, **kwds) + + +class HitBox: + def __init__(self, points: PointList): + self._points = points + + @property + def points(self): + return self._points + + def create_adjustable( + self, + position: Tuple[float, float] = (0.0, 0.0), + rotation: float = 0.0, + scale: Tuple[float, float] = (1.0, 1.0), + ) -> AdjustableHitBox: + return AdjustableHitBox( + self._points, position=position, rotation=rotation, scale=scale + ) + + def get_adjusted_points(self): + return self.points + + +class AdjustableHitBox(HitBox): + def __init__( + self, + points: PointList, + *, + position: Tuple[float, float] = (0.0, 0.0), + rotation: float = 0.0, + scale: Tuple[float, float] = (1.0, 1.0), + ): + super().__init__(points) + self._position = position + self._rotation = rotation + self._scale = scale + + self._left = None + self._right = None + self._bottom = None + self._top = None + + self._adjusted_points = None + self._adjusted_cache_dirty = True + + @property + def position(self): + return self._position + + @property + def left(self): + points = self.get_adjusted_points() + x_points = [point[0] for point in points] + return min(x_points) + + @property + def right(self): + points = self.get_adjusted_points() + x_points = [point[0] for point in points] + return max(x_points) + + @property + def top(self): + points = self.get_adjusted_points() + y_points = [point[1] for point in points] + return max(y_points) + + @property + def bottom(self): + points = self.get_adjusted_points() + y_points = [point[1] for point in points] + return min(y_points) + + @property + def x(self): + return self._x + + @x.setter + def x(self, x: float): + self._x = x + self._adjusted_cache_dirty = True + + @property + def y(self): + return self._y + + @y.setter + def y(self, y: float): + self._y = y + self._adjusted_cache_dirty = True + + @property + def rotation(self): + return self._rotation + + @rotation.setter + def rotation(self, rotation: float): + self._rotation = rotation + self._adjusted_cache_dirty = True + + @property + def scale(self): + return self._scale + + @scale.setter + def scale(self, scale: Tuple[float, float]): + self._scale = scale + self._adjusted_cache_dirty = True + + def get_adjusted_points(self): + if not self._adjusted_cache_dirty: + return self._adjusted_points + + rad = radians(self._rotation) + rad_cos = cos(rad) + rad_sin = sin(rad) + + def _adjust_point(point) -> Point: + x, y = point + + x *= self.scale[0] + y *= self.scale[1] + + if rad: + x = x * rad_cos - y * rad_sin + y = x * rad_sin + y * rad_cos + + return ( + x + self.position[0], + y + self.position[1], + ) + + self._adjusted_points = tuple([_adjust_point(point) for point in self.points]) + self._adjusted_cache_dirty = False + return self._adjusted_points diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index 0d5dc45a8..ff90c0bcf 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -1,23 +1,16 @@ import math -from math import sin, cos, radians -from typing import ( - Any, - Dict, - List, - Optional, - TYPE_CHECKING, -) +from math import cos, radians, sin from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, List, Optional +from arcade import Texture, load_texture +from arcade.hitbox import AdjustableHitBox, HitBox from arcade.math import get_angle_degrees -from arcade import ( - load_texture, - Texture, -) from arcade.texture import get_default_texture -from arcade.types import Point, PointList, PathOrTexture -from .mixins import PymunkMixin +from arcade.types import PathOrTexture, Point, PointList + from .base import BasicSprite +from .mixins import PymunkMixin if TYPE_CHECKING: # handle import cycle caused by type hinting from arcade.sprite_list import SpriteList @@ -35,6 +28,7 @@ class Sprite(BasicSprite, PymunkMixin): :param float scale: Scale the image up or down. Scale of 1.0 is none. :param float angle: The initial rotation of the sprite in degrees """ + __slots__ = ( "_velocity", "change_angle", @@ -100,8 +94,9 @@ def __init__( self.cur_texture_index: int = 0 self.textures: List[Texture] = _textures - self._hit_box_points: Optional[PointList] = None - self._hit_box_points_cache: Optional[PointList] = None + self._hit_box: AdjustableHitBox = self._texture.hit_box.create_adjustable( + position=self._position, rotation=self.angle, scale=self._scale + ) self.physics_engines: List[Any] = [] self._sprite_list: Optional[SpriteList] = None @@ -110,11 +105,24 @@ def __init__( self._width = self._texture.width * scale self._height = self._texture.height * scale - if not self._hit_box_points: - self._hit_box_points = self._texture.hit_box_points # --- Properties --- + @BasicSprite.center_x.setter + def center_x(self, new_value: float): + BasicSprite.center_x.fset(self, new_value) + self._hit_box._position = (new_value, self._position[1]) + + @BasicSprite.center_y.setter + def center_y(self, new_value: float): + BasicSprite.center_y.fset(self, new_value) + self._hit_box._position = (self._position[0], new_value) + + @BasicSprite.position.setter + def position(self, new_value: Point): + BasicSprite.position.fset(self, new_value) + self._hit_box._position = new_value + @property def left(self) -> float: """ @@ -123,15 +131,7 @@ def left(self) -> float: When setting this property the sprite is positioned relative to the leftmost x coordinate in the hit box. """ - points = self.get_adjusted_hit_box() - - # This happens if our point list is empty, such as a completely - # transparent sprite. - # if len(points) == 0: - # return self.center_x - - x_points = [point[0] for point in points] - return min(x_points) + return self._hit_box.left @left.setter def left(self, amount: float): @@ -147,15 +147,7 @@ def right(self) -> float: When setting this property the sprite is positioned relative to the rightmost x coordinate in the hit box. """ - points = self.get_adjusted_hit_box() - - # This happens if our point list is empty, such as a completely - # transparent sprite. - # if len(points) == 0: - # return self.center_x - - x_points = [point[0] for point in points] - return max(x_points) + return self._hit_box.right @right.setter def right(self, amount: float): @@ -171,15 +163,7 @@ def bottom(self) -> float: When setting this property the sprite is positioned relative to the lowest y coordinate in the hit box. """ - points = self.get_adjusted_hit_box() - - # This happens if our point list is empty, such as a completely - # transparent sprite. - # if len(points) == 0: - # return self.center_y - - y_points = [point[1] for point in points] - return min(y_points) + return self._hit_box.bottom @bottom.setter def bottom(self, amount: float): @@ -195,9 +179,7 @@ def top(self) -> float: When setting this property the sprite is positioned relative to the highest y coordinate in the hit box. """ - points = self.get_adjusted_hit_box() - y_points = [point[1] for point in points] - return max(y_points) + return self._hit_box.top @top.setter def top(self, amount: float): @@ -220,6 +202,7 @@ def angle(self, new_value: float): return self._angle = new_value + self._hit_box.rotation = new_value for sprite_list in self.sprite_lists: sprite_list._update_angle(self) @@ -279,15 +262,15 @@ def change_y(self, new_value: float): self._velocity = self._velocity[0], new_value @property - def hit_box(self) -> PointList: + def hit_box(self) -> HitBox: """ Get or set the hit box for this sprite. """ return self.get_hit_box() @hit_box.setter - def hit_box(self, points: PointList): - self.set_hit_box(points) + def hit_box(self, hit_box: HitBox): + self.set_hit_box(hit_box) @property def texture(self) -> Texture: @@ -300,12 +283,16 @@ def texture(self, texture: Texture): return if __debug__ and not isinstance(texture, Texture): - raise TypeError(f"The 'texture' parameter must be an instance of arcade.Texture," - f" but is an instance of '{type(texture)}'.") + raise TypeError( + f"The 'texture' parameter must be an instance of arcade.Texture," + f" but is an instance of '{type(texture)}'." + ) # If sprite is using default texture, update the hit box if self._texture is get_default_texture(): - self.hit_box = texture.hit_box_points + self.hit_box = texture.hit_box.create_adjustable( + position=self._position, rotation=self.angle, scale=self._scale + ) self._texture = texture self._width = texture.width * self._scale[0] @@ -331,17 +318,16 @@ def properties(self, value): # --- Hitbox methods ----- - def set_hit_box(self, points: PointList) -> None: + def set_hit_box(self, hit_box: AdjustableHitBox) -> None: """ Set a sprite's hit box. Hit box should be relative to a sprite's center, and with a scale of 1.0. Points will be scaled and rotated with ``get_adjusted_hit_box``. """ - self._hit_box_points_cache = None - self._hit_box_points = points + self._hit_box = hit_box - def get_hit_box(self) -> PointList: + def get_hit_box(self) -> HitBox: """ Use the hit_box property to get or set a sprite's hit box. Hit boxes are specified assuming the sprite's center is at (0, 0). @@ -354,57 +340,10 @@ def get_hit_box(self) -> PointList: Specify a hit box unadjusted for translation, rotation, or scale. You can get an adjusted hit box with :class:`arcade.Sprite.get_adjusted_hit_box`. """ - # Use existing points if we have them - if self._hit_box_points is not None: - return self._hit_box_points - - # If we don't already have points, try to get them from the texture - if self._texture: - self._hit_box_points = self._texture.hit_box_points - else: - raise ValueError("Sprite has no hit box points due to missing texture") - - return self._hit_box_points + return self._hit_box def get_adjusted_hit_box(self) -> PointList: - """ - Get the hit box adjusted for translation, rotation, and scale. - - The result is cached internally for performance reasons. - """ - # If we've already calculated the adjusted hit box, use the cached version - if self._hit_box_points_cache is not None: - return self._hit_box_points_cache - - rad = radians(self._angle) - scale_x, scale_y = self._scale - position_x, position_y = self._position - rad_cos = cos(rad) - rad_sin = sin(rad) - - def _adjust_point(point) -> Point: - x, y = point - - # Apply scaling - x *= scale_x - y *= scale_y - - # Rotate the point if needed - if rad: - rot_x = x * rad_cos - y * rad_sin - rot_y = x * rad_sin + y * rad_cos - x = rot_x - y = rot_y - - # Apply position - return ( - x + position_x, - y + position_y, - ) - - # Cache the results - self._hit_box_points_cache = tuple([_adjust_point(point) for point in self.get_hit_box()]) - return self._hit_box_points_cache + return self._hit_box.get_adjusted_points() # --- Movement methods ----- @@ -476,7 +415,6 @@ def face_point(self, point: Point) -> None: # Reverse angle because sprite angles are backwards self.angle = -angle - # ---- Draw Methods ---- def draw(self, *, filter=None, pixelated=None, blend_function=None) -> None: @@ -492,10 +430,13 @@ def draw(self, *, filter=None, pixelated=None, blend_function=None) -> None: """ if self._sprite_list is None: from arcade import SpriteList + self._sprite_list = SpriteList(capacity=1) - + self._sprite_list.append(self) - self._sprite_list.draw(filter=filter, pixelated=pixelated, blend_function=blend_function) + self._sprite_list.draw( + filter=filter, pixelated=pixelated, blend_function=blend_function + ) self._sprite_list.remove(self) # ----Update Methods ---- @@ -518,7 +459,7 @@ def update_spatial_hash(self) -> None: """ Update the sprites location in the spatial hash. """ - self._hit_box_points_cache = None + self._hit_box._adjusted_cache_dirty = True # super().update_spatial_hash() for sprite_list in self.sprite_lists: if sprite_list.spatial_hash is not None: @@ -569,3 +510,4 @@ def register_physics_engine(self, physics_engine) -> None: or a custom one you made. """ self.physics_engines.append(physics_engine) + self.physics_engines.append(physics_engine) diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index 081487011..735e54a1e 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -121,7 +121,7 @@ def _check_for_collision(sprite1: SpriteType, sprite2: SpriteType) -> bool: return False return are_polygons_intersecting( - sprite1.get_adjusted_hit_box(), sprite2.get_adjusted_hit_box() + sprite1.hit_box, sprite2.hit_box ) @@ -290,7 +290,7 @@ def get_sprites_at_point(point: Point, sprite_list: SpriteList[SpriteType]) -> L return [ s for s in sprites_to_check - if is_point_in_polygon(point[0], point[1], s.get_adjusted_hit_box()) + if is_point_in_polygon(point[0], point[1], s.hit_box) ] @@ -359,5 +359,5 @@ def get_sprites_in_rect(rect: Rect, sprite_list: SpriteList) -> List[SpriteType] return [ s for s in sprites_to_check - if are_polygons_intersecting(rect_points, s.get_adjusted_hit_box()) + if are_polygons_intersecting(rect_points, s.hit_box) ] diff --git a/arcade/texture/texture.py b/arcade/texture/texture.py index 418a52a71..1eafb2214 100644 --- a/arcade/texture/texture.py +++ b/arcade/texture/texture.py @@ -22,7 +22,7 @@ ) from arcade.types import PointList from arcade.color import TRANSPARENT_BLACK -from arcade.hitbox import HitBoxAlgorithm +from arcade.hitbox import HitBoxAlgorithm, HitBox from arcade import cache as _cache from arcade import hitbox @@ -120,7 +120,7 @@ class Texture: :param PIL.Image.Image image: The image or ImageData for this texture :param str hit_box_algorithm: The algorithm to use for calculating the hit box. - :param PointList hit_box_points: List of points for the hit box (Optional). + :param HitBox hit_box: A HitBox for the texture to use (Optional). Completely overrides the hit box algorithm. :param str hash: Optional unique name for the texture. Can be used to make this texture globally unique. By default the hash of the pixel data is used. @@ -132,7 +132,7 @@ class Texture: "_transforms", "_sprite_list", "_hit_box_algorithm", - "_hit_box_points", + "_hit_box", "_hash", "_cache_name", "_atlas_name", @@ -147,7 +147,7 @@ def __init__( image: Union[PIL.Image.Image, ImageData], *, hit_box_algorithm: Optional[HitBoxAlgorithm] = None, - hit_box_points: Optional[PointList] = None, + hit_box: Optional[HitBox] = None, hash: Optional[str] = None, **kwargs, ): @@ -184,7 +184,7 @@ def __init__( self._cache_name: str = "" self._atlas_name: str = "" self._update_cache_names() - self._hit_box_points: PointList = hit_box_points or self._calculate_hit_box_points() + self._hit_box: HitBox = hit_box or self._calculate_hit_box() # Track what atlases the image is in self._atlas_refs: Optional[WeakSet["TextureAtlas"]] = None @@ -388,7 +388,7 @@ def size(self, value: Tuple[int, int]): self._size = value @property - def hit_box_points(self) -> PointList: + def hit_box(self) -> HitBox: """ Get the hit box points for this texture. @@ -397,7 +397,7 @@ def hit_box_points(self) -> PointList: :return: PointList """ - return self._hit_box_points + return self._hit_box @property def hit_box_algorithm(self) -> HitBoxAlgorithm: @@ -624,11 +624,11 @@ def transform( :param Transform transform: Transform to apply :return: New texture """ - new_points = transform.transform_hit_box_points(self._hit_box_points) + new_hit_box = HitBox(transform.transform_hit_box_points(self._hit_box.points)) texture = Texture( self.image_data, hit_box_algorithm=self._hit_box_algorithm, - hit_box_points=new_points, + hit_box=new_hit_box, hash=self._hash, ) texture.width = self.width @@ -740,7 +740,7 @@ def validate_crop(image: PIL.Image.Image, x: int, y: int, width: int, height: in if y + height - 1 >= image.height: raise ValueError(f"height is outside of texture: {height + y}") - def _calculate_hit_box_points(self) -> PointList: + def _calculate_hit_box(self) -> HitBox: """ Calculate the hit box points for this texture based on the configured hit box algorithm. This is usually done on texture creation @@ -749,14 +749,14 @@ def _calculate_hit_box_points(self) -> PointList: # Check if we have cached points points = _cache.hit_box_cache.get(self.cache_name) if points: - return points + return HitBox(points) # Calculate points with the selected algorithm points = self._hit_box_algorithm.calculate(self.image) if self._hit_box_algorithm.cache: _cache.hit_box_cache.put(self.cache_name, points) - return points + return HitBox(points) # ----- Drawing functions ----- diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index 00d9df1e3..ed0df284b 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -24,7 +24,7 @@ SpriteList, get_window, ) -from arcade.hitbox import HitBoxAlgorithm +from arcade.hitbox import HitBoxAlgorithm, AdjustableHitBox from arcade.texture.loading import _load_tilemap_texture if TYPE_CHECKING: @@ -560,7 +560,7 @@ def _create_sprite_from_tile( for point in points: point[0], point[1] = point[1], point[0] - my_sprite.hit_box = points + my_sprite.hit_box = AdjustableHitBox(points, position=my_sprite.position, rotation=my_sprite.angle, scale=my_sprite.scale_xy) if tile.animation: key_frame_list = [] From 442a8b6f2ec46cd05c28c2a2ad83245737d3a5fe Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 16 Mar 2023 18:32:05 -0400 Subject: [PATCH 02/10] More changes to accomodate rust, this is very broken --- arcade/examples/platform_tutorial/17_views.py | 44 ++++++++++--------- arcade/hitbox/base.py | 21 ++++----- arcade/sprite/sprite.py | 20 +++++---- arcade/texture/texture.py | 2 +- arcade/tilemap/tilemap.py | 4 +- 5 files changed, 49 insertions(+), 42 deletions(-) diff --git a/arcade/examples/platform_tutorial/17_views.py b/arcade/examples/platform_tutorial/17_views.py index 64d11f3a3..7376520c9 100644 --- a/arcade/examples/platform_tutorial/17_views.py +++ b/arcade/examples/platform_tutorial/17_views.py @@ -5,6 +5,10 @@ """ import math +import arcade_accelerate + +arcade_accelerate.bootstrap() + import arcade # Constants @@ -59,9 +63,7 @@ def load_texture_pair(filename): Load a texture pair, with the second being a mirror image. """ texture = arcade.load_texture(filename) - return [ - texture, texture.flip_left_right() - ] + return [texture, texture.flip_left_right()] class Entity(arcade.Sprite): @@ -100,12 +102,15 @@ def __init__(self, name_folder, name_file): # Hit box will be set based on the first image used. If you want to specify # a different hit box, you can do it like the code below. # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]]) - self.set_hit_box(self.texture.hit_box.create_adjustable(self.position, self.angle, self.scale_xy)) + self.set_hit_box( + self.texture.hit_box.create_adjustable( + self.position, self.angle, self.scale_xy + ) + ) class Enemy(Entity): def __init__(self, name_folder, name_file): - # Setup parent class super().__init__(name_folder, name_file) @@ -113,7 +118,6 @@ def __init__(self, name_folder, name_file): self.health = 0 def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.facing_direction == RIGHT_FACING: self.facing_direction = LEFT_FACING @@ -139,7 +143,6 @@ def update_animation(self, delta_time: float = 1 / 60): class RobotEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("robot", "robot") @@ -148,7 +151,6 @@ def __init__(self): class ZombieEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("zombie", "zombie") @@ -159,7 +161,6 @@ class PlayerCharacter(Entity): """Player Sprite""" def __init__(self): - # Set up parent class super().__init__("male_person", "malePerson") @@ -169,7 +170,6 @@ def __init__(self): self.is_on_ladder = False def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.facing_direction == RIGHT_FACING: self.facing_direction = LEFT_FACING @@ -381,7 +381,7 @@ def setup(self): platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS], gravity_constant=GRAVITY, ladders=self.scene[LAYER_NAME_LADDERS], - walls=self.scene[LAYER_NAME_PLATFORMS] + walls=self.scene[LAYER_NAME_PLATFORMS], ) def on_show_view(self): @@ -400,9 +400,10 @@ def on_draw(self): self.scene.draw() # Draw hit boxes. - self.scene[LAYER_NAME_COINS].draw_hit_boxes(color=arcade.color.WHITE) - self.scene[LAYER_NAME_ENEMIES].draw_hit_boxes(color=arcade.color.WHITE) - self.scene[LAYER_NAME_PLAYER].draw_hit_boxes(color=arcade.color.WHITE) + # self.scene[LAYER_NAME_COINS].draw_hit_boxes(color=arcade.color.WHITE) + # self.scene[LAYER_NAME_ENEMIES].draw_hit_boxes(color=arcade.color.WHITE) + # self.scene[LAYER_NAME_PLAYER].draw_hit_boxes(color=arcade.color.WHITE) + self.scene.draw_hit_boxes(color=arcade.color.WHITE) # Activate the GUI camera before drawing GUI elements self.gui_camera.use() @@ -498,8 +499,10 @@ def on_mouse_scroll(self, x, y, scroll_x, scroll_y): pass def center_camera_to_player(self, speed=0.2): - screen_center_x = (self.player_sprite.center_x - (self.camera.viewport_width / 2)) - screen_center_y = (self.player_sprite.center_y - (self.camera.viewport_height / 2)) + screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) + screen_center_y = self.player_sprite.center_y - ( + self.camera.viewport_height / 2 + ) if screen_center_x < 0: screen_center_x = 0 if screen_center_y < 0: @@ -598,10 +601,7 @@ def on_update(self, delta_time): bullet.remove_from_sprite_lists() for collision in hit_list: - if ( - self.scene[LAYER_NAME_ENEMIES] - in collision.sprite_lists - ): + if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists: # The collision was with an enemy collision.health -= BULLET_DAMAGE @@ -630,7 +630,6 @@ def on_update(self, delta_time): # Loop through each coin we hit (if any) and remove it for collision in player_collision_list: - if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists: arcade.play_sound(self.game_over) game_over = GameOverView() @@ -687,3 +686,6 @@ def main(): if __name__ == "__main__": main() +if __name__ == "__main__": + main() + main() diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index 6e9129edf..720c66ac8 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -46,11 +46,11 @@ def points(self): def create_adjustable( self, position: Tuple[float, float] = (0.0, 0.0), - rotation: float = 0.0, + angle: float = 0.0, scale: Tuple[float, float] = (1.0, 1.0), ) -> AdjustableHitBox: return AdjustableHitBox( - self._points, position=position, rotation=rotation, scale=scale + self._points, position=position, angle=angle, scale=scale ) def get_adjusted_points(self): @@ -63,12 +63,12 @@ def __init__( points: PointList, *, position: Tuple[float, float] = (0.0, 0.0), - rotation: float = 0.0, + angle: float = 0.0, scale: Tuple[float, float] = (1.0, 1.0), ): super().__init__(points) self._position = position - self._rotation = rotation + self._angle = angle self._scale = scale self._left = None @@ -126,12 +126,12 @@ def y(self, y: float): self._adjusted_cache_dirty = True @property - def rotation(self): - return self._rotation + def angle(self): + return self._angle - @rotation.setter - def rotation(self, rotation: float): - self._rotation = rotation + @angle.setter + def angle(self, angle: float): + self._angle = angle self._adjusted_cache_dirty = True @property @@ -147,7 +147,7 @@ def get_adjusted_points(self): if not self._adjusted_cache_dirty: return self._adjusted_points - rad = radians(self._rotation) + rad = radians(self._angle) rad_cos = cos(rad) rad_sin = sin(rad) @@ -169,3 +169,4 @@ def _adjust_point(point) -> Point: self._adjusted_points = tuple([_adjust_point(point) for point in self.points]) self._adjusted_cache_dirty = False return self._adjusted_points + return self._adjusted_points diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index ff90c0bcf..d4c741fdc 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -94,9 +94,13 @@ def __init__( self.cur_texture_index: int = 0 self.textures: List[Texture] = _textures - self._hit_box: AdjustableHitBox = self._texture.hit_box.create_adjustable( - position=self._position, rotation=self.angle, scale=self._scale + self._hit_box: AdjustableHitBox = AdjustableHitBox( + points=self._texture.hit_box.points, + position=self._position, + angle=self.angle, + scale=self._scale, ) + print(dir(self._hit_box)) self.physics_engines: List[Any] = [] self._sprite_list: Optional[SpriteList] = None @@ -111,17 +115,17 @@ def __init__( @BasicSprite.center_x.setter def center_x(self, new_value: float): BasicSprite.center_x.fset(self, new_value) - self._hit_box._position = (new_value, self._position[1]) + self._hit_box.position = (new_value, self._position[1]) @BasicSprite.center_y.setter def center_y(self, new_value: float): BasicSprite.center_y.fset(self, new_value) - self._hit_box._position = (self._position[0], new_value) + self._hit_box.position = (self._position[0], new_value) @BasicSprite.position.setter def position(self, new_value: Point): BasicSprite.position.fset(self, new_value) - self._hit_box._position = new_value + self._hit_box.position = new_value @property def left(self) -> float: @@ -202,7 +206,7 @@ def angle(self, new_value: float): return self._angle = new_value - self._hit_box.rotation = new_value + self._hit_box.angle = new_value for sprite_list in self.sprite_lists: sprite_list._update_angle(self) @@ -291,7 +295,7 @@ def texture(self, texture: Texture): # If sprite is using default texture, update the hit box if self._texture is get_default_texture(): self.hit_box = texture.hit_box.create_adjustable( - position=self._position, rotation=self.angle, scale=self._scale + position=self._position, angle=self.angle, scale=self._scale ) self._texture = texture @@ -459,7 +463,7 @@ def update_spatial_hash(self) -> None: """ Update the sprites location in the spatial hash. """ - self._hit_box._adjusted_cache_dirty = True + # self._hit_box._adjusted_cache_dirty = True # super().update_spatial_hash() for sprite_list in self.sprite_lists: if sprite_list.spatial_hash is not None: diff --git a/arcade/texture/texture.py b/arcade/texture/texture.py index 1eafb2214..19d1f6f28 100644 --- a/arcade/texture/texture.py +++ b/arcade/texture/texture.py @@ -22,7 +22,7 @@ ) from arcade.types import PointList from arcade.color import TRANSPARENT_BLACK -from arcade.hitbox import HitBoxAlgorithm, HitBox +from arcade.hitbox.base import HitBoxAlgorithm, HitBox from arcade import cache as _cache from arcade import hitbox diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index ed0df284b..5f7c74d57 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -560,7 +560,7 @@ def _create_sprite_from_tile( for point in points: point[0], point[1] = point[1], point[0] - my_sprite.hit_box = AdjustableHitBox(points, position=my_sprite.position, rotation=my_sprite.angle, scale=my_sprite.scale_xy) + my_sprite.hit_box = AdjustableHitBox(points, position=my_sprite.position, angle=my_sprite.angle, scale=my_sprite.scale_xy) if tile.animation: key_frame_list = [] @@ -834,7 +834,7 @@ def _process_object_layer( angle_degrees = math.degrees(rotation) rotated_center_x, rotated_center_y = rotate_point( - width / 2, height / 2, 0, 0, angle_degrees + (width / 2, height / 2), (0, 0), angle_degrees ) my_sprite.position = (x + rotated_center_x, y + rotated_center_y) From 9f6f4f5f93dad58d0096d632b95106f0d9ff8cd8 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 16 Mar 2023 18:59:53 -0400 Subject: [PATCH 03/10] Rust <-> Python API compatibility --- arcade/hitbox/base.py | 5 +++++ arcade/sprite/sprite.py | 5 +++-- arcade/tilemap/tilemap.py | 11 ++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index 720c66ac8..278a7a98d 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -83,6 +83,11 @@ def __init__( def position(self): return self._position + @position.setter + def position(self, position: Point): + self._position = position + self._adjusted_cache_dirty = True + @property def left(self): points = self.get_adjusted_points() diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index d4c741fdc..94557bc3b 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -100,7 +100,6 @@ def __init__( angle=self.angle, scale=self._scale, ) - print(dir(self._hit_box)) self.physics_engines: List[Any] = [] self._sprite_list: Optional[SpriteList] = None @@ -151,7 +150,9 @@ def right(self) -> float: When setting this property the sprite is positioned relative to the rightmost x coordinate in the hit box. """ - return self._hit_box.right + value = self._hit_box.right + print(value) + return value @right.setter def right(self, amount: float): diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index 5f7c74d57..d8cee417e 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -24,7 +24,7 @@ SpriteList, get_window, ) -from arcade.hitbox import HitBoxAlgorithm, AdjustableHitBox +from arcade.hitbox import AdjustableHitBox, HitBoxAlgorithm from arcade.texture.loading import _load_tilemap_texture if TYPE_CHECKING: @@ -560,7 +560,12 @@ def _create_sprite_from_tile( for point in points: point[0], point[1] = point[1], point[0] - my_sprite.hit_box = AdjustableHitBox(points, position=my_sprite.position, angle=my_sprite.angle, scale=my_sprite.scale_xy) + my_sprite.hit_box = AdjustableHitBox( + points, + position=my_sprite.position, + angle=my_sprite.angle, + scale=my_sprite.scale_xy, + ) if tile.animation: key_frame_list = [] @@ -834,7 +839,7 @@ def _process_object_layer( angle_degrees = math.degrees(rotation) rotated_center_x, rotated_center_y = rotate_point( - (width / 2, height / 2), (0, 0), angle_degrees + width / 2, height / 2, 0, 0, angle_degrees ) my_sprite.position = (x + rotated_center_x, y + rotated_center_y) From 5893a3faa0094aa74313c69a8629df4a718d5787 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 16 Mar 2023 19:44:39 -0400 Subject: [PATCH 04/10] API compatibility --- arcade/geometry/geometry_python.py | 31 ++++++++++++++---------------- arcade/sprite/sprite.py | 1 - arcade/sprite_list/collision.py | 9 +++++---- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/arcade/geometry/geometry_python.py b/arcade/geometry/geometry_python.py index 6ae370dac..8f5ec9438 100644 --- a/arcade/geometry/geometry_python.py +++ b/arcade/geometry/geometry_python.py @@ -5,11 +5,11 @@ Point in polygon function from https://www.geeksforgeeks.org/how-to-check-if-a-given-point-lies-inside-a-polygon/ """ -from arcade.types import Point from arcade.hitbox import HitBox +from arcade.types import Point, PointList -def are_polygons_intersecting(poly_a: HitBox, poly_b: HitBox) -> bool: +def are_polygons_intersecting(poly_a: PointList, poly_b: PointList) -> bool: """ Return True if two polygons intersect. @@ -18,21 +18,20 @@ def are_polygons_intersecting(poly_a: HitBox, poly_b: HitBox) -> bool: :Returns: True or false depending if polygons intersect :rtype bool: """ - points_a = poly_a.get_adjusted_points() - points_b = poly_b.get_adjusted_points() - for polygon in (points_a, points_b): - + for polygon in (poly_a, poly_b): for i1 in range(len(polygon)): i2 = (i1 + 1) % len(polygon) projection_1 = polygon[i1] projection_2 = polygon[i2] - normal = (projection_2[1] - projection_1[1], - projection_1[0] - projection_2[0]) + normal = ( + projection_2[1] - projection_1[1], + projection_1[0] - projection_2[0], + ) min_a, max_a, min_b, max_b = (None,) * 4 - for poly in points_a: + for poly in poly_a: projected = normal[0] * poly[0] + normal[1] * poly[1] if min_a is None or projected < min_a: @@ -40,7 +39,7 @@ def are_polygons_intersecting(poly_a: HitBox, poly_b: HitBox) -> bool: if max_a is None or projected > max_a: max_a = projected - for poly in points_b: + for poly in poly_b: projected = normal[0] * poly[0] + normal[1] * poly[1] if min_b is None or projected < min_b: @@ -87,7 +86,7 @@ def get_triangle_orientation(p: Point, q: Point, r: Point) -> int: :param Point r: Point 3 :Returns: 0, 1, or 2 depending on orientation """ - val = (((q[1] - p[1]) * (r[0] - q[0])) - ((q[0] - p[0]) * (r[1] - q[1]))) + val = ((q[1] - p[1]) * (r[0] - q[0])) - ((q[0] - p[0]) * (r[1] - q[1])) if val == 0: return 0 # collinear @@ -174,16 +173,14 @@ def is_point_in_polygon(x: float, y: float, polygon: HitBox) -> bool: # Check if the line segment from 'p' to # 'extreme' intersects with the line # segment from 'polygon[i]' to 'polygon[next]' - if (are_lines_intersecting(points[i], - points[next_item], - p, extreme)): + if are_lines_intersecting(points[i], points[next_item], p, extreme): # If the point 'p' is collinear with line # segment 'i-next', then check if it lies # on segment. If it lies, return true, otherwise false - if get_triangle_orientation(points[i], p, - points[next_item]) == 0: + if get_triangle_orientation(points[i], p, points[next_item]) == 0: return not is_point_in_box( - points[i], p, + points[i], + p, points[next_item], ) diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index 94557bc3b..75f318e79 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -151,7 +151,6 @@ def right(self) -> float: relative to the rightmost x coordinate in the hit box. """ value = self._hit_box.right - print(value) return value @right.setter diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index 735e54a1e..fa8d0f137 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -9,14 +9,15 @@ from arcade import ( get_window, ) -from arcade.math import get_distance from arcade.geometry import ( are_polygons_intersecting, is_point_in_polygon, ) +from arcade.math import get_distance +from arcade.sprite import BasicSprite, SpriteType from arcade.types import Point, Rect + from .sprite_list import SpriteList -from arcade.sprite import SpriteType, BasicSprite def get_distance_between_sprites(sprite1: SpriteType, sprite2: SpriteType) -> float: @@ -121,7 +122,7 @@ def _check_for_collision(sprite1: SpriteType, sprite2: SpriteType) -> bool: return False return are_polygons_intersecting( - sprite1.hit_box, sprite2.hit_box + sprite1.hit_box.get_adjusted_points(), sprite2.hit_box.get_adjusted_points() ) @@ -359,5 +360,5 @@ def get_sprites_in_rect(rect: Rect, sprite_list: SpriteList) -> List[SpriteType] return [ s for s in sprites_to_check - if are_polygons_intersecting(rect_points, s.hit_box) + if are_polygons_intersecting(rect_points, s.hit_box.get_adjusted_points) ] From 96c9e836875c600f97d77b7e1314eecb406f728b Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Fri, 17 Mar 2023 01:58:44 -0400 Subject: [PATCH 05/10] bunch of work on failing tests --- arcade/sprite/colored.py | 29 +++++----- arcade/sprite/sprite.py | 70 +++++++++---------------- arcade/sprite_list/collision.py | 4 +- tests/unit/sprite/test_sprite_hitbox.py | 51 +++++++++++------- tests/unit/texture/test_textures.py | 4 +- 5 files changed, 75 insertions(+), 83 deletions(-) diff --git a/arcade/sprite/colored.py b/arcade/sprite/colored.py index d15b569c9..cb5c2487b 100644 --- a/arcade/sprite/colored.py +++ b/arcade/sprite/colored.py @@ -1,16 +1,13 @@ import PIL -from .sprite import Sprite import arcade +from arcade import cache, hitbox +from arcade.hitbox import HitBox +from arcade.texture import (ImageData, Texture, make_circle_texture, + make_soft_circle_texture) from arcade.types import Color -from arcade import cache -from arcade import hitbox -from arcade.texture import ( - make_circle_texture, - make_soft_circle_texture, - Texture, - ImageData, -) + +from .sprite import Sprite class SpriteSolidColor(Sprite): @@ -47,11 +44,13 @@ def __init__( ): texture = Texture( self._default_image, - hit_box_points=( - (-width / 2, -height / 2), - (width / 2, -height / 2), - (width / 2, height / 2), - (-width / 2, height / 2) + hit_box=HitBox( + ( + (-width / 2, -height / 2), + (width / 2, -height / 2), + (width / 2, height / 2), + (-width / 2, height / 2) + ) ) ) texture.size = width, height @@ -119,4 +118,4 @@ def __init__(self, radius: int, color: Color, soft: bool = False, **kwargs): # apply results to the new sprite super().__init__(texture) self.color = color_rgba - self._points = self.texture.hit_box_points + self._points = self.texture.hit_box.points diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index 75f318e79..b04a0dc74 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -1,13 +1,12 @@ import math -from math import cos, radians, sin from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from arcade import Texture, load_texture from arcade.hitbox import AdjustableHitBox, HitBox from arcade.math import get_angle_degrees from arcade.texture import get_default_texture -from arcade.types import PathOrTexture, Point, PointList +from arcade.types import PathOrTexture, Point from .base import BasicSprite from .mixins import PymunkMixin @@ -39,8 +38,7 @@ class Sprite(BasicSprite, PymunkMixin): "boundary_bottom", "textures", "cur_texture_index", - "_hit_box_points", - "_hit_box_points_cache", + "_hit_box", "physics_engines", "_sprite_list", "guid", @@ -77,6 +75,12 @@ def __init__( ) PymunkMixin.__init__(self) + self._hit_box: AdjustableHitBox = self._texture.hit_box.create_adjustable( + position=self._position, + angle=self.angle, + scale=self._scale, + ) + self.angle = angle # Movement self._velocity = 0.0, 0.0 @@ -94,12 +98,6 @@ def __init__( self.cur_texture_index: int = 0 self.textures: List[Texture] = _textures - self._hit_box: AdjustableHitBox = AdjustableHitBox( - points=self._texture.hit_box.points, - position=self._position, - angle=self.angle, - scale=self._scale, - ) self.physics_engines: List[Any] = [] self._sprite_list: Optional[SpriteList] = None @@ -126,6 +124,11 @@ def position(self, new_value: Point): BasicSprite.position.fset(self, new_value) self._hit_box.position = new_value + @BasicSprite.scale.setter + def scale(self, new_value: float): + BasicSprite.scale.fset(self, new_value) + self._hit_box.scale = (new_value, new_value) + @property def left(self) -> float: """ @@ -150,8 +153,7 @@ def right(self) -> float: When setting this property the sprite is positioned relative to the rightmost x coordinate in the hit box. """ - value = self._hit_box.right - return value + return self._hit_box.right @right.setter def right(self, amount: float): @@ -270,11 +272,18 @@ def hit_box(self) -> HitBox: """ Get or set the hit box for this sprite. """ - return self.get_hit_box() + return self._hit_box @hit_box.setter - def hit_box(self, hit_box: HitBox): - self.set_hit_box(hit_box) + def hit_box(self, hit_box: Union[HitBox, AdjustableHitBox]): + if type(hit_box) == HitBox: + self._hit_box = hit_box.create_adjustable( + self.position, self.angle, self.scale_xy + ) + else: + # Mypy doesn't seem to understand the type check above + # It still thinks hit_box can be a union here + self._hit_box = hit_box # type: ignore @property def texture(self) -> Texture: @@ -320,35 +329,6 @@ def properties(self) -> Dict[str, Any]: def properties(self, value): self._properties = value - # --- Hitbox methods ----- - - def set_hit_box(self, hit_box: AdjustableHitBox) -> None: - """ - Set a sprite's hit box. Hit box should be relative to a sprite's center, - and with a scale of 1.0. - - Points will be scaled and rotated with ``get_adjusted_hit_box``. - """ - self._hit_box = hit_box - - def get_hit_box(self) -> HitBox: - """ - Use the hit_box property to get or set a sprite's hit box. - Hit boxes are specified assuming the sprite's center is at (0, 0). - Specify hit boxes like: - - .. code-block:: - - mySprite.hit_box = [[-10, -10], [10, -10], [10, 10]] - - Specify a hit box unadjusted for translation, rotation, or scale. - You can get an adjusted hit box with :class:`arcade.Sprite.get_adjusted_hit_box`. - """ - return self._hit_box - - def get_adjusted_hit_box(self) -> PointList: - return self._hit_box.get_adjusted_points() - # --- Movement methods ----- def forward(self, speed: float = 1.0) -> None: diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index fa8d0f137..e2d1aad11 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -291,7 +291,7 @@ def get_sprites_at_point(point: Point, sprite_list: SpriteList[SpriteType]) -> L return [ s for s in sprites_to_check - if is_point_in_polygon(point[0], point[1], s.hit_box) + if is_point_in_polygon(point[0], point[1], s.hit_box.get_adjusted_points) ] @@ -360,5 +360,5 @@ def get_sprites_in_rect(rect: Rect, sprite_list: SpriteList) -> List[SpriteType] return [ s for s in sprites_to_check - if are_polygons_intersecting(rect_points, s.hit_box.get_adjusted_points) + if are_polygons_intersecting(rect_points, s.hit_box.get_adjusted_points()) ] diff --git a/tests/unit/sprite/test_sprite_hitbox.py b/tests/unit/sprite/test_sprite_hitbox.py index 2f5240610..6f6eb87d4 100644 --- a/tests/unit/sprite/test_sprite_hitbox.py +++ b/tests/unit/sprite/test_sprite_hitbox.py @@ -4,37 +4,39 @@ def test_1(): # setup - my_sprite = arcade.Sprite(arcade.make_soft_square_texture(20, arcade.color.RED, 0, 255)) - hit_box = [-10, -10], [-10, 10], [10, 10], [10, -10] - my_sprite.set_hit_box(hit_box) + my_sprite = arcade.Sprite( + arcade.make_soft_square_texture(20, arcade.color.RED, 0, 255) + ) + hit_box = arcade.hitbox.HitBox(([-10, -10], [-10, 10], [10, 10], [10, -10])) + my_sprite.hit_box = hit_box my_sprite.scale = 1.0 my_sprite.angle = 0 my_sprite.center_x = 100 my_sprite.center_y = 100 print() - hitbox = my_sprite.get_adjusted_hit_box() - print(f'Hitbox: {my_sprite.scale} -> {my_sprite._hit_box_points} -> {hitbox}') + hitbox = my_sprite.hit_box.get_adjusted_points() + print(f"Hitbox: {my_sprite.scale} -> {my_sprite.hit_box.points} -> {hitbox}") assert hitbox == ((90.0, 90.0), (90.0, 110.0), (110.0, 110.0), (110.0, 90.0)) my_sprite.scale = 0.5 - hitbox = my_sprite.get_adjusted_hit_box() - print(f'Hitbox: {my_sprite.scale} -> {my_sprite._hit_box_points} -> {hitbox}') + hitbox = my_sprite.hit_box.get_adjusted_points() + print(f"Hitbox: {my_sprite.scale} -> {my_sprite.hit_box.points} -> {hitbox}") assert hitbox == ((95.0, 95.0), (95.0, 105.0), (105.0, 105.0), (105.0, 95.0)) my_sprite.scale = 1 - hitbox = my_sprite.get_adjusted_hit_box() - print(f'Hitbox: {my_sprite.scale} -> {my_sprite._hit_box_points} -> {hitbox}') + hitbox = my_sprite.hit_box.get_adjusted_points() + print(f"Hitbox: {my_sprite.scale} -> {my_sprite.hit_box.points} -> {hitbox}") assert hitbox == ((90.0, 90.0), (90.0, 110.0), (110.0, 110.0), (110.0, 90.0)) my_sprite.scale = 2.0 - hitbox = my_sprite.get_adjusted_hit_box() - print(f'Hitbox: {my_sprite.scale} -> {my_sprite._hit_box_points} -> {hitbox}') + hitbox = my_sprite.hit_box.get_adjusted_points() + print(f"Hitbox: {my_sprite.scale} -> {my_sprite.hit_box.points} -> {hitbox}") assert hitbox == ((80.0, 80.0), (80.0, 120.0), (120.0, 120.0), (120.0, 80.0)) my_sprite.scale = 2.0 - hitbox = my_sprite.get_adjusted_hit_box() - print(f'Hitbox: {my_sprite.scale} -> {my_sprite._hit_box_points} -> {hitbox}') + hitbox = my_sprite.hit_box.get_adjusted_points() + print(f"Hitbox: {my_sprite.scale} -> {my_sprite.hit_box.points} -> {hitbox}") assert hitbox == ((80.0, 80.0), (80.0, 120.0), (120.0, 120.0), (120.0, 80.0)) @@ -50,7 +52,7 @@ def test_2(): assert wall.bottom == -height / 2 assert wall.left == -width / 2 assert wall.right == width / 2 - hit_box = wall.get_hit_box() + hit_box = wall.hit_box.points assert hit_box[0] == (-width / 2, -height / 2) assert hit_box[1] == (width / 2, -height / 2) assert hit_box[2] == (width / 2, height / 2) @@ -67,7 +69,7 @@ def test_2(): assert wall.bottom == -height / 2 assert wall.left == -width / 2 assert wall.right == width / 2 - hit_box = wall.get_hit_box() + hit_box = wall.hit_box.points assert hit_box[0] == (-width / 2, -height / 2) assert hit_box[1] == (width / 2, -height / 2) assert hit_box[2] == (width / 2, height / 2) @@ -84,15 +86,26 @@ def test_2(): assert wall.bottom == -height / 2 assert wall.left == -width / 2 assert wall.right == width / 2 - hit_box = wall.get_hit_box() + hit_box = wall.hit_box.points assert hit_box[0] == (-width / 2, -height / 2) assert hit_box[1] == (width / 2, -height / 2) assert hit_box[2] == (width / 2, height / 2) assert hit_box[3] == (-width / 2, height / 2) - texture = arcade.load_texture(":resources:images/items/coinGold.png", hit_box_algorithm=hitbox.algo_detailed) + texture = arcade.load_texture( + ":resources:images/items/coinGold.png", hit_box_algorithm=hitbox.algo_detailed + ) wall = arcade.Sprite(texture) wall.position = 0, 0 - hit_box = wall.get_hit_box() - assert hit_box == ((-32, 7), (-17, 28), (7, 32), (29, 15), (32, -7), (17, -28), (-8, -32), (-28, -17)) + hit_box = wall.hit_box.points + assert hit_box == ( + (-32, 7), + (-17, 28), + (7, 32), + (29, 15), + (32, -7), + (17, -28), + (-8, -32), + (-28, -17), + ) diff --git a/tests/unit/texture/test_textures.py b/tests/unit/texture/test_textures.py index 338f6aca4..205366b0a 100644 --- a/tests/unit/texture/test_textures.py +++ b/tests/unit/texture/test_textures.py @@ -65,7 +65,7 @@ def test_load_texture(): assert tex.width == 128 assert tex.height == 128 assert tex.size == (128, 128) - assert tex.hit_box_points is not None + assert tex.hit_box is not None assert tex._sprite_list is None with pytest.raises(FileNotFoundError): @@ -165,7 +165,7 @@ def test_crate_empty(): assert tex.crop_values is None assert tex.size == size assert tex._hit_box_algorithm == hitbox.algo_bounding_box - assert tex.hit_box_points == ( + assert tex.hit_box.points == ( (-128.0, -128.0), (128.0, -128.0), (128.0, 128.0), From cd7ad87948de7ce41fbfe8d3c7cdd624681dd4b8 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sat, 18 Mar 2023 00:29:33 -0400 Subject: [PATCH 06/10] Bunch of testing fixes --- arcade/geometry/geometry_python.py | 24 +++++++++------------ arcade/hitbox/base.py | 25 ++++------------------ arcade/pymunk_physics_engine.py | 2 +- arcade/sprite/base.py | 5 +++++ arcade/sprite/sprite.py | 10 +++++++++ arcade/sprite_list/collision.py | 2 +- tests/unit/sprite/test_sprite_collision.py | 2 +- 7 files changed, 32 insertions(+), 38 deletions(-) diff --git a/arcade/geometry/geometry_python.py b/arcade/geometry/geometry_python.py index 8f5ec9438..aff1e3253 100644 --- a/arcade/geometry/geometry_python.py +++ b/arcade/geometry/geometry_python.py @@ -5,7 +5,6 @@ Point in polygon function from https://www.geeksforgeeks.org/how-to-check-if-a-given-point-lies-inside-a-polygon/ """ -from arcade.hitbox import HitBox from arcade.types import Point, PointList @@ -19,15 +18,14 @@ def are_polygons_intersecting(poly_a: PointList, poly_b: PointList) -> bool: :rtype bool: """ for polygon in (poly_a, poly_b): + for i1 in range(len(polygon)): i2 = (i1 + 1) % len(polygon) projection_1 = polygon[i1] projection_2 = polygon[i2] - normal = ( - projection_2[1] - projection_1[1], - projection_1[0] - projection_2[0], - ) + normal = (projection_2[1] - projection_1[1], + projection_1[0] - projection_2[0]) min_a, max_a, min_b, max_b = (None,) * 4 @@ -53,7 +51,6 @@ def are_polygons_intersecting(poly_a: PointList, poly_b: PointList) -> bool: return True - def is_point_in_box(p: Point, q: Point, r: Point) -> bool: """ Return True if point q is inside the box defined by p and r. @@ -136,7 +133,7 @@ def are_lines_intersecting(p1: Point, q1: Point, p2: Point, q2: Point) -> bool: return False -def is_point_in_polygon(x: float, y: float, polygon: HitBox) -> bool: +def is_point_in_polygon(x: float, y: float, polygon: PointList) -> bool: """ Checks if a point is inside a polygon of three or more points. @@ -145,9 +142,8 @@ def is_point_in_polygon(x: float, y: float, polygon: HitBox) -> bool: :param PointList polygon_point_list: List of points that define the polygon. :Returns: True or false depending if point is inside polygon """ - points = polygon.get_adjusted_points() p = x, y - n = len(points) + n = len(polygon) # There must be at least 3 vertices # in polygon @@ -167,21 +163,21 @@ def is_point_in_polygon(x: float, y: float, polygon: HitBox) -> bool: while True: next_item = (i + 1) % n - if points[i][1] == p[1]: + if polygon[i][1] == p[1]: decrease += 1 # Check if the line segment from 'p' to # 'extreme' intersects with the line # segment from 'polygon[i]' to 'polygon[next]' - if are_lines_intersecting(points[i], points[next_item], p, extreme): + if are_lines_intersecting(polygon[i], polygon[next_item], p, extreme): # If the point 'p' is collinear with line # segment 'i-next', then check if it lies # on segment. If it lies, return true, otherwise false - if get_triangle_orientation(points[i], p, points[next_item]) == 0: + if get_triangle_orientation(polygon[i], p, polygon[next_item]) == 0: return not is_point_in_box( - points[i], + polygon[i], p, - points[next_item], + polygon[next_item], ) count += 1 diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index 278a7a98d..460e1dbea 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -112,24 +112,6 @@ def bottom(self): y_points = [point[1] for point in points] return min(y_points) - @property - def x(self): - return self._x - - @x.setter - def x(self, x: float): - self._x = x - self._adjusted_cache_dirty = True - - @property - def y(self): - return self._y - - @y.setter - def y(self, y: float): - self._y = y - self._adjusted_cache_dirty = True - @property def angle(self): return self._angle @@ -163,8 +145,10 @@ def _adjust_point(point) -> Point: y *= self.scale[1] if rad: - x = x * rad_cos - y * rad_sin - y = x * rad_sin + y * rad_cos + rot_x = x * rad_cos - y * rad_sin + rot_y = x * rad_sin + y * rad_cos + x = rot_x + y = rot_y return ( x + self.position[0], @@ -174,4 +158,3 @@ def _adjust_point(point) -> Point: self._adjusted_points = tuple([_adjust_point(point) for point in self.points]) self._adjusted_cache_dirty = False return self._adjusted_points - return self._adjusted_points diff --git a/arcade/pymunk_physics_engine.py b/arcade/pymunk_physics_engine.py index d7e262c20..246474d06 100644 --- a/arcade/pymunk_physics_engine.py +++ b/arcade/pymunk_physics_engine.py @@ -172,7 +172,7 @@ def velocity_callback(my_body: pymunk.Body, my_gravity: Tuple[float, float], my_ body.velocity_func = velocity_callback # Set the physics shape to the sprite's hitbox - poly = sprite.get_hit_box() + poly = sprite.hit_box.points scaled_poly = [[x * sprite.scale for x in z] for z in poly] shape = pymunk.Poly(body, scaled_poly, radius=radius) # type: ignore diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 4b1cebd7b..97f729c75 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -3,6 +3,7 @@ import arcade from arcade.types import RGBA, Point, PointList, Color from arcade.color import BLACK +from arcade.hitbox import HitBox from arcade.texture import Texture if TYPE_CHECKING: from arcade.sprite_list import SpriteList @@ -511,6 +512,10 @@ def rescale_xy_relative_to_point( # ---- Utility Methods ---- + @property + def hit_box(self) -> HitBox: + return self._texture.hit_box + def get_adjusted_hit_box(self) -> PointList: """ Return the hit box points adjusted for the sprite's position. diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index b04a0dc74..f76d530e4 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -129,6 +129,16 @@ def scale(self, new_value: float): BasicSprite.scale.fset(self, new_value) self._hit_box.scale = (new_value, new_value) + @BasicSprite.width.setter + def width(self, new_value: float): + BasicSprite.width.fset(self, new_value) + self._hit_box.scale = self._scale + + @BasicSprite.height.setter + def height(self, new_value: float): + BasicSprite.height.fset(self, new_value) + self._hit_box.scale = self._scale + @property def left(self) -> float: """ diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index e2d1aad11..193658674 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -291,7 +291,7 @@ def get_sprites_at_point(point: Point, sprite_list: SpriteList[SpriteType]) -> L return [ s for s in sprites_to_check - if is_point_in_polygon(point[0], point[1], s.hit_box.get_adjusted_points) + if is_point_in_polygon(point[0], point[1], s.hit_box.get_adjusted_points()) ] diff --git a/tests/unit/sprite/test_sprite_collision.py b/tests/unit/sprite/test_sprite_collision.py index 677135500..2926b2da0 100644 --- a/tests/unit/sprite/test_sprite_collision.py +++ b/tests/unit/sprite/test_sprite_collision.py @@ -247,9 +247,9 @@ def test_get_sprites_at_point(window): # With spatial hash sp = arcade.SpriteList(use_spatial_hash=True) - sp.extend((a, b)) a.position = 0, 0 b.position = 0, 0 + sp.extend((a, b)) assert set(arcade.get_sprites_at_point((0, 0), sp)) == set([a, b]) b.position = 1000, 0 assert set(arcade.get_sprites_at_point((0, 0), sp)) == set([a]) From f83b107773187741ae444ee4ee3c42645bed3442 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sat, 18 Mar 2023 00:57:23 -0400 Subject: [PATCH 07/10] Tests and linting fixes --- arcade/examples/platform_tutorial/17_views.py | 4 - arcade/hitbox/__init__.py | 7 +- arcade/hitbox/base.py | 2 +- arcade/sprite/base.py | 92 ++++++++++--------- arcade/sprite/sprite.py | 12 ++- arcade/texture/texture.py | 1 - arcade/texture_atlas/helpers.py | 10 +- 7 files changed, 70 insertions(+), 58 deletions(-) diff --git a/arcade/examples/platform_tutorial/17_views.py b/arcade/examples/platform_tutorial/17_views.py index 7376520c9..ce0ac11ff 100644 --- a/arcade/examples/platform_tutorial/17_views.py +++ b/arcade/examples/platform_tutorial/17_views.py @@ -5,10 +5,6 @@ """ import math -import arcade_accelerate - -arcade_accelerate.bootstrap() - import arcade # Constants diff --git a/arcade/hitbox/__init__.py b/arcade/hitbox/__init__.py index be4c9b297..9460e5111 100644 --- a/arcade/hitbox/__init__.py +++ b/arcade/hitbox/__init__.py @@ -1,9 +1,11 @@ from PIL.Image import Image + from arcade.types import PointList -from .base import HitBoxAlgorithm, HitBox, AdjustableHitBox + +from .base import AdjustableHitBox, HitBox, HitBoxAlgorithm from .bounding_box import BoundingHitBoxAlgorithm -from .simple import SimpleHitBoxAlgorithm from .pymunk import PymunkHitBoxAlgorithm +from .simple import SimpleHitBoxAlgorithm #: The simple hit box algorithm. algo_simple = SimpleHitBoxAlgorithm() @@ -48,6 +50,7 @@ def calculate_hit_box_points_detailed( __all__ = [ "HitBoxAlgorithm", "HitBox", + "AdjustableHitBox", "SimpleHitBoxAlgorithm", "PymunkHitBoxAlgorithm", "BoundingHitBoxAlgorithm", diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index 460e1dbea..11e840660 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -132,7 +132,7 @@ def scale(self, scale: Tuple[float, float]): def get_adjusted_points(self): if not self._adjusted_cache_dirty: - return self._adjusted_points + return self._adjusted_points rad = radians(self._angle) rad_cos = cos(rad) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 97f729c75..930bfd7c7 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -1,10 +1,11 @@ -from typing import TYPE_CHECKING, List, Iterable, TypeVar +from typing import TYPE_CHECKING, Iterable, List, TypeVar import arcade -from arcade.types import RGBA, Point, PointList, Color from arcade.color import BLACK from arcade.hitbox import HitBox from arcade.texture import Texture +from arcade.types import RGBA, Color, Point, PointList + if TYPE_CHECKING: from arcade.sprite_list import SpriteList @@ -16,6 +17,7 @@ class BasicSprite: """ The absolute minimum needed for a sprite. """ + __slots__ = ( "_position", "_depth", @@ -41,7 +43,7 @@ def __init__( self._depth = 0.0 self._texture = texture self._width = texture.width * scale - self._height =texture.height * scale + self._height = texture.height * scale self._scale = scale, scale self._color: RGBA = 255, 255, 255, 255 self.sprite_lists: List["SpriteList"] = [] @@ -51,8 +53,7 @@ def __init__( # --- Core Properties --- - @property - def position(self) -> Point: + def _get_position(self) -> Point: """ Get or set the center x and y position of the sprite. @@ -61,8 +62,7 @@ def position(self) -> Point: """ return self._position - @position.setter - def position(self, new_value: Point): + def _set_position(self, new_value: Point): if new_value == self._position: return @@ -72,13 +72,13 @@ def position(self, new_value: Point): for sprite_list in self.sprite_lists: sprite_list._update_position(self) - @property - def center_x(self) -> float: + position = property(_get_position, _set_position) + + def _get_center_x(self) -> float: """Get or set the center x position of the sprite.""" return self._position[0] - @center_x.setter - def center_x(self, new_value: float): + def _set_center_x(self, new_value: float): if new_value == self._position[0]: return @@ -88,13 +88,13 @@ def center_x(self, new_value: float): for sprite_list in self.sprite_lists: sprite_list._update_position_x(self) - @property - def center_y(self) -> float: + center_x = property(_get_center_x, _set_center_x) + + def _get_center_y(self) -> float: """Get or set the center y position of the sprite.""" return self._position[1] - @center_y.setter - def center_y(self, new_value: float): + def _set_center_y(self, new_value: float): if new_value == self._position[1]: return @@ -104,11 +104,13 @@ def center_y(self, new_value: float): for sprite_list in self.sprite_lists: sprite_list._update_position_y(self) + center_y = property(_get_center_y, _set_center_y) + @property def depth(self) -> float: """ Get or set the depth of the sprite. - + This is really the z coordinate of the sprite and can be used with OpenGL depth testing with opaque sprites. @@ -122,13 +124,11 @@ def depth(self, new_value: float): for sprite_list in self.sprite_lists: sprite_list._update_depth(self) - @property - def width(self) -> float: + def _get_width(self) -> float: """Get or set width or the sprite in pixels""" return self._width - @width.setter - def width(self, new_value: float): + def _set_width(self, new_value: float): if new_value != self._width: self._scale = new_value / self._texture.width, self._scale[1] self._width = new_value @@ -137,13 +137,13 @@ def width(self, new_value: float): for sprite_list in self.sprite_lists: sprite_list._update_width(self) - @property - def height(self) -> float: + width = property(_get_width, _set_width) + + def _get_height(self) -> float: """Get or set the height of the sprite in pixels.""" return self._height - @height.setter - def height(self, new_value: float): + def _set_height(self, new_value: float): if new_value != self._height: self._scale = self._scale[0], new_value / self._texture.height self._height = new_value @@ -152,6 +152,8 @@ def height(self, new_value: float): for sprite_list in self.sprite_lists: sprite_list._update_height(self) + height = property(_get_height, _set_height) + # @property # def size(self) -> Point: # """Get or set the size of the sprite as a pair of values.""" @@ -168,8 +170,7 @@ def height(self, new_value: float): # for sprite_list in self.sprite_lists: # sprite_list._update_size(self) - @property - def scale(self) -> float: + def _get_scale(self) -> float: """ Get or set the sprite's x scale value or set both x & y scale to the same value. @@ -178,8 +179,7 @@ def scale(self) -> float: """ return self._scale[0] - @scale.setter - def scale(self, new_value: float): + def _set_scale(self, new_value: float): if new_value == self._scale[0] and new_value == self._scale[1]: return @@ -192,6 +192,8 @@ def scale(self, new_value: float): for sprite_list in self.sprite_lists: sprite_list._update_size(self) + scale = property(_get_scale, _set_scale) + @property def scale_xy(self) -> Point: """Get or set the x & y scale of the sprite as a pair of values.""" @@ -296,7 +298,12 @@ def visible(self) -> bool: @visible.setter def visible(self, value: bool): - self._color = self._color[0], self._color[1], self._color[2], 255 if value else 0 + self._color = ( + self._color[0], + self._color[1], + self._color[2], + 255 if value else 0, + ) for sprite_list in self.sprite_lists: sprite_list._update_color(self) @@ -332,7 +339,7 @@ def color(self, color: RGBA): and self._color[2] == color[2] ): return - self._color = color[0], color[1], color[2], self._color[3] + self._color = color[0], color[1], color[2], self._color[3] else: raise ValueError("Color must be three or four ints from 0-255") @@ -367,8 +374,10 @@ def texture(self, texture: Texture): return if __debug__ and not isinstance(texture, Texture): - raise TypeError(f"The 'texture' parameter must be an instance of arcade.Texture," - f" but is an instance of '{type(texture)}'.") + raise TypeError( + f"The 'texture' parameter must be an instance of arcade.Texture," + f" but is an instance of '{type(texture)}'." + ) self._texture = texture self._width = texture.width * self._scale[0] @@ -390,7 +399,7 @@ def on_update(self, delta_time: float = 1 / 60) -> None: """ Update the sprite. Similar to update, but also takes a delta-time. It can be called manually or by the SpriteList's on_update method. - + :param float delta_time: Time since last update. """ pass @@ -401,7 +410,7 @@ def update_animation(self, delta_time: float = 1 / 60) -> None: the active texture on the sprite. This can be called manually or by the SpriteList's update_animation method. - + :param float delta_time: Time since last update. """ pass @@ -443,7 +452,7 @@ def rescale_relative_to_point(self, point: Point, factor: float) -> None: if position_changed: self._position = ( (self._position[0] - point[0]) * factor + point[0], - (self._position[1] - point[1]) * factor + point[1] + (self._position[1] - point[1]) * factor + point[1], ) # rebuild all spatial metadata @@ -454,9 +463,7 @@ def rescale_relative_to_point(self, point: Point, factor: float) -> None: sprite_list._update_position(self) def rescale_xy_relative_to_point( - self, - point: Point, - factors_xy: Iterable[float] + self, point: Point, factors_xy: Iterable[float] ) -> None: """ Rescale the sprite and its distance from the passed point. @@ -500,7 +507,7 @@ def rescale_xy_relative_to_point( if position_changed: self._position = ( (self._position[0] - point[0]) * factor_x + point[0], - (self._position[1] - point[1]) * factor_y + point[1] + (self._position[1] - point[1]) * factor_y + point[1], ) # rebuild all spatial metadata @@ -527,7 +534,7 @@ def get_adjusted_hit_box(self) -> PointList: (-w / 2 + x, -h / 2 + y), (w / 2 + x, -h / 2 + y), (w / 2 + x, h / 2 + y), - (-w / 2 + x, h / 2 + y) + (-w / 2 + x, h / 2 + y), ) def update_spatial_hash(self) -> None: @@ -600,7 +607,9 @@ def collides_with_sprite(self: SpriteType, other: SpriteType) -> bool: return check_for_collision(self, other) - def collides_with_list(self: SpriteType, sprite_list: "SpriteList") -> List[SpriteType]: + def collides_with_list( + self: SpriteType, sprite_list: "SpriteList" + ) -> List[SpriteType]: """Check if current sprite is overlapping with any other sprite in a list :param SpriteList sprite_list: SpriteList to check against @@ -611,3 +620,4 @@ def collides_with_list(self: SpriteType, sprite_list: "SpriteList") -> List[Spri # noinspection PyTypeChecker return check_for_collision_with_list(self, sprite_list) + return check_for_collision_with_list(self, sprite_list) diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index f76d530e4..fbdeab121 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -111,18 +111,18 @@ def __init__( @BasicSprite.center_x.setter def center_x(self, new_value: float): - BasicSprite.center_x.fset(self, new_value) self._hit_box.position = (new_value, self._position[1]) + BasicSprite.center_x.fset(self, new_value) @BasicSprite.center_y.setter def center_y(self, new_value: float): - BasicSprite.center_y.fset(self, new_value) self._hit_box.position = (self._position[0], new_value) + BasicSprite.center_y.fset(self, new_value) @BasicSprite.position.setter def position(self, new_value: Point): - BasicSprite.position.fset(self, new_value) self._hit_box.position = new_value + BasicSprite.position.fset(self, new_value) @BasicSprite.scale.setter def scale(self, new_value: float): @@ -131,13 +131,15 @@ def scale(self, new_value: float): @BasicSprite.width.setter def width(self, new_value: float): + new_scale = new_value / self._texture.width, self._scale[0] + self._hit_box.scale = new_scale BasicSprite.width.fset(self, new_value) - self._hit_box.scale = self._scale @BasicSprite.height.setter def height(self, new_value: float): + new_scale = new_value / self._texture.height, self._scale[1] + self._hit_box.scale = new_scale BasicSprite.height.fset(self, new_value) - self._hit_box.scale = self._scale @property def left(self) -> float: diff --git a/arcade/texture/texture.py b/arcade/texture/texture.py index 19d1f6f28..6d0b55221 100644 --- a/arcade/texture/texture.py +++ b/arcade/texture/texture.py @@ -20,7 +20,6 @@ TransverseTransform, ORIENTATIONS ) -from arcade.types import PointList from arcade.color import TRANSPARENT_BLACK from arcade.hitbox.base import HitBoxAlgorithm, HitBox from arcade import cache as _cache diff --git a/arcade/texture_atlas/helpers.py b/arcade/texture_atlas/helpers.py index 211a7d310..cf9a33c08 100644 --- a/arcade/texture_atlas/helpers.py +++ b/arcade/texture_atlas/helpers.py @@ -1,12 +1,13 @@ import json -from typing import Dict, Tuple from pathlib import Path from time import perf_counter +from typing import Dict, Tuple import arcade -from .base import TextureAtlas, AtlasRegion -from arcade.texture import Texture, ImageData from arcade import cache +from arcade.texture import ImageData, Texture + +from .base import AtlasRegion, TextureAtlas class FakeImage: @@ -70,7 +71,7 @@ def save_atlas(atlas: TextureAtlas, directory: Path, name: str, resource_root: P "hash": texture.image_data.hash, "path": texture.file_path.relative_to(resource_root).as_posix(), "crop": texture.crop_values, - "points": texture.hit_box_points, + "points": texture.hit_box.points, "region": _dump_region_info(atlas.get_texture_region_info(texture.atlas_name)), "vertex_order": texture._vertex_order, }) @@ -175,3 +176,4 @@ def load_atlas( atlas.use_uv_texture() return atlas, perf_data + return atlas, perf_data From 1adc3d66ac0152f559c5afeee16f30aad5186e55 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sat, 18 Mar 2023 01:18:31 -0400 Subject: [PATCH 08/10] HitBox API updates --- .../platform_tutorial/12_animate_character.py | 7 ++----- .../platform_tutorial/13_add_enemies.py | 10 ++-------- .../platform_tutorial/14_moving_enemies.py | 11 ++--------- .../15_collision_with_enemies.py | 12 ++---------- .../platform_tutorial/16_shooting_bullets.py | 17 +++-------------- arcade/examples/platform_tutorial/17_views.py | 6 +----- arcade/hitbox/base.py | 15 +++++++++++++-- arcade/sprite/base.py | 18 ++---------------- 8 files changed, 27 insertions(+), 69 deletions(-) diff --git a/arcade/examples/platform_tutorial/12_animate_character.py b/arcade/examples/platform_tutorial/12_animate_character.py index ea5ba1d16..937d1946d 100644 --- a/arcade/examples/platform_tutorial/12_animate_character.py +++ b/arcade/examples/platform_tutorial/12_animate_character.py @@ -51,7 +51,6 @@ class PlayerCharacter(arcade.Sprite): """Player Sprite""" def __init__(self): - # Set up parent class super().__init__() @@ -96,10 +95,9 @@ def __init__(self): # Hit box will be set based on the first image used. If you want to specify # a different hit box, you can do it like the code below. # set_hit_box = [[-22, -64], [22, -64], [22, 28], [-22, 28]] - self.hit_box = self.texture.hit_box_points + self.hit_box = self.texture.hit_box.adjustable(self) def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.character_face_direction == RIGHT_FACING: self.character_face_direction = LEFT_FACING @@ -245,7 +243,7 @@ def setup(self): platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS], gravity_constant=GRAVITY, ladders=self.scene[LAYER_NAME_LADDERS], - walls=self.scene[LAYER_NAME_PLATFORMS] + walls=self.scene[LAYER_NAME_PLATFORMS], ) def on_draw(self): @@ -389,7 +387,6 @@ def on_update(self, delta_time): # Loop through each coin we hit (if any) and remove it for coin in coin_hit_list: - # Figure out how many points this coin is worth if "Points" not in coin.properties: print("Warning, collected a coin without a Points property.") diff --git a/arcade/examples/platform_tutorial/13_add_enemies.py b/arcade/examples/platform_tutorial/13_add_enemies.py index e0e09049a..5176a363d 100644 --- a/arcade/examples/platform_tutorial/13_add_enemies.py +++ b/arcade/examples/platform_tutorial/13_add_enemies.py @@ -87,26 +87,23 @@ def __init__(self, name_folder, name_file): # Hit box will be set based on the first image used. If you want to specify # a different hit box, you can do it like the code below. # set_hit_box = [[-22, -64], [22, -64], [22, 28], [-22, 28]] - self.hit_box = self.texture.hit_box_points + self.hit_box = self.texture.hit_box.adjustable(self) class Enemy(Entity): def __init__(self, name_folder, name_file): - # Setup parent class super().__init__(name_folder, name_file) class RobotEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("robot", "robot") class ZombieEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("zombie", "zombie") @@ -115,7 +112,6 @@ class PlayerCharacter(Entity): """Player Sprite""" def __init__(self): - # Set up parent class super().__init__("male_person", "malePerson") @@ -125,7 +121,6 @@ def __init__(self): self.is_on_ladder = False def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.facing_direction == RIGHT_FACING: self.facing_direction = LEFT_FACING @@ -295,7 +290,7 @@ def setup(self): platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS], gravity_constant=GRAVITY, ladders=self.scene[LAYER_NAME_LADDERS], - walls=self.scene[LAYER_NAME_PLATFORMS] + walls=self.scene[LAYER_NAME_PLATFORMS], ) def on_draw(self): @@ -439,7 +434,6 @@ def on_update(self, delta_time): # Loop through each coin we hit (if any) and remove it for coin in coin_hit_list: - # Figure out how many points this coin is worth if "Points" not in coin.properties: print("Warning, collected a coin without a Points property.") diff --git a/arcade/examples/platform_tutorial/14_moving_enemies.py b/arcade/examples/platform_tutorial/14_moving_enemies.py index 13174818a..c66a381ad 100644 --- a/arcade/examples/platform_tutorial/14_moving_enemies.py +++ b/arcade/examples/platform_tutorial/14_moving_enemies.py @@ -93,19 +93,17 @@ def __init__(self, name_folder, name_file): # Hit box will be set based on the first image used. If you want to specify # a different hit box, you can do it like the code below. # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]]) - self.set_hit_box(self.texture.hit_box_points) + self.hit_box = self.texture.hit_box.adjustable(self) class Enemy(Entity): def __init__(self, name_folder, name_file): - # Setup parent class super().__init__(name_folder, name_file) self.should_update_walk = 0 def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.facing_direction == RIGHT_FACING: self.facing_direction = LEFT_FACING @@ -131,14 +129,12 @@ def update_animation(self, delta_time: float = 1 / 60): class RobotEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("robot", "robot") class ZombieEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("zombie", "zombie") @@ -147,7 +143,6 @@ class PlayerCharacter(Entity): """Player Sprite""" def __init__(self): - # Set up parent class super().__init__("male_person", "malePerson") @@ -157,7 +152,6 @@ def __init__(self): self.is_on_ladder = False def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.facing_direction == RIGHT_FACING: self.facing_direction = LEFT_FACING @@ -331,7 +325,7 @@ def setup(self): platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS], gravity_constant=GRAVITY, ladders=self.scene[LAYER_NAME_LADDERS], - walls=self.scene[LAYER_NAME_PLATFORMS] + walls=self.scene[LAYER_NAME_PLATFORMS], ) def on_draw(self): @@ -497,7 +491,6 @@ def on_update(self, delta_time): # Loop through each coin we hit (if any) and remove it for coin in coin_hit_list: - # Figure out how many points this coin is worth if "Points" not in coin.properties: print("Warning, collected a coin without a Points property.") diff --git a/arcade/examples/platform_tutorial/15_collision_with_enemies.py b/arcade/examples/platform_tutorial/15_collision_with_enemies.py index 96e40e891..7fc7cf912 100644 --- a/arcade/examples/platform_tutorial/15_collision_with_enemies.py +++ b/arcade/examples/platform_tutorial/15_collision_with_enemies.py @@ -92,20 +92,17 @@ def __init__(self, name_folder, name_file): # Hit box will be set based on the first image used. If you want to specify # a different hit box, you can do it like the code below. - # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]]) - self.set_hit_box(self.texture.hit_box_points) + self.hit_box = self.texture.hit_box.adjustable(self) class Enemy(Entity): def __init__(self, name_folder, name_file): - # Setup parent class super().__init__(name_folder, name_file) self.should_update_walk = 0 def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.facing_direction == RIGHT_FACING: self.facing_direction = LEFT_FACING @@ -131,14 +128,12 @@ def update_animation(self, delta_time: float = 1 / 60): class RobotEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("robot", "robot") class ZombieEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("zombie", "zombie") @@ -147,7 +142,6 @@ class PlayerCharacter(Entity): """Player Sprite""" def __init__(self): - # Set up parent class super().__init__("male_person", "malePerson") @@ -157,7 +151,6 @@ def __init__(self): self.is_on_ladder = False def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.facing_direction == RIGHT_FACING: self.facing_direction = LEFT_FACING @@ -331,7 +324,7 @@ def setup(self): platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS], gravity_constant=GRAVITY, ladders=self.scene[LAYER_NAME_LADDERS], - walls=self.scene[LAYER_NAME_PLATFORMS] + walls=self.scene[LAYER_NAME_PLATFORMS], ) def on_draw(self): @@ -505,7 +498,6 @@ def on_update(self, delta_time): # Loop through each coin we hit (if any) and remove it for collision in player_collision_list: - if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists: arcade.play_sound(self.game_over) self.setup() diff --git a/arcade/examples/platform_tutorial/16_shooting_bullets.py b/arcade/examples/platform_tutorial/16_shooting_bullets.py index 86a0c4f17..a41953c0e 100644 --- a/arcade/examples/platform_tutorial/16_shooting_bullets.py +++ b/arcade/examples/platform_tutorial/16_shooting_bullets.py @@ -99,13 +99,11 @@ def __init__(self, name_folder, name_file): # Hit box will be set based on the first image used. If you want to specify # a different hit box, you can do it like the code below. - # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]]) - self.set_hit_box(self.texture.hit_box_points) + self.hit_box = self.texture.hit_box.adjustable(self) class Enemy(Entity): def __init__(self, name_folder, name_file): - # Setup parent class super().__init__(name_folder, name_file) @@ -113,7 +111,6 @@ def __init__(self, name_folder, name_file): self.health = 0 def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.facing_direction == RIGHT_FACING: self.facing_direction = LEFT_FACING @@ -139,7 +136,6 @@ def update_animation(self, delta_time: float = 1 / 60): class RobotEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("robot", "robot") @@ -148,7 +144,6 @@ def __init__(self): class ZombieEnemy(Enemy): def __init__(self): - # Set up parent class super().__init__("zombie", "zombie") @@ -159,7 +154,6 @@ class PlayerCharacter(Entity): """Player Sprite""" def __init__(self): - # Set up parent class super().__init__("male_person", "malePerson") @@ -169,7 +163,6 @@ def __init__(self): self.is_on_ladder = False def update_animation(self, delta_time: float = 1 / 60): - # Figure out if we need to flip face left or right if self.change_x < 0 and self.facing_direction == RIGHT_FACING: self.facing_direction = LEFT_FACING @@ -357,7 +350,7 @@ def setup(self): platforms=self.scene[LAYER_NAME_MOVING_PLATFORMS], gravity_constant=GRAVITY, ladders=self.scene[LAYER_NAME_LADDERS], - walls=self.scene[LAYER_NAME_PLATFORMS] + walls=self.scene[LAYER_NAME_PLATFORMS], ) def on_draw(self): @@ -563,10 +556,7 @@ def on_update(self, delta_time): bullet.remove_from_sprite_lists() for collision in hit_list: - if ( - self.scene[LAYER_NAME_ENEMIES] - in collision.sprite_lists - ): + if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists: # The collision was with an enemy collision.health -= BULLET_DAMAGE @@ -595,7 +585,6 @@ def on_update(self, delta_time): # Loop through each coin we hit (if any) and remove it for collision in player_collision_list: - if self.scene[LAYER_NAME_ENEMIES] in collision.sprite_lists: arcade.play_sound(self.game_over) self.setup() diff --git a/arcade/examples/platform_tutorial/17_views.py b/arcade/examples/platform_tutorial/17_views.py index ce0ac11ff..b9ac97f22 100644 --- a/arcade/examples/platform_tutorial/17_views.py +++ b/arcade/examples/platform_tutorial/17_views.py @@ -98,11 +98,7 @@ def __init__(self, name_folder, name_file): # Hit box will be set based on the first image used. If you want to specify # a different hit box, you can do it like the code below. # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]]) - self.set_hit_box( - self.texture.hit_box.create_adjustable( - self.position, self.angle, self.scale_xy - ) - ) + self.hit_box = self.texture.hit_box.adjustable(self) class Enemy(Entity): diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index 11e840660..d12397cbb 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -1,12 +1,15 @@ from __future__ import annotations from math import cos, radians, sin -from typing import Any, Tuple +from typing import TYPE_CHECKING, Any, Tuple from PIL.Image import Image from arcade.types import Point, PointList +if TYPE_CHECKING: + from arcade.sprite import Sprite + class HitBoxAlgorithm: """ @@ -53,6 +56,14 @@ def create_adjustable( self._points, position=position, angle=angle, scale=scale ) + def adjustable(self, sprite: Sprite) -> AdjustableHitBox: + return AdjustableHitBox( + self._points, + position=sprite.position, + angle=sprite.angle, + scale=sprite.scale_xy, + ) + def get_adjusted_points(self): return self.points @@ -132,7 +143,7 @@ def scale(self, scale: Tuple[float, float]): def get_adjusted_points(self): if not self._adjusted_cache_dirty: - return self._adjusted_points + return self._adjusted_points rad = radians(self._angle) rad_cos = cos(rad) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 930bfd7c7..6e3066e8d 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -523,20 +523,6 @@ def rescale_xy_relative_to_point( def hit_box(self) -> HitBox: return self._texture.hit_box - def get_adjusted_hit_box(self) -> PointList: - """ - Return the hit box points adjusted for the sprite's position. - """ - x, y = self._position - w, h = self._width, self._height - # TODO: Might might want to cache this? - return ( - (-w / 2 + x, -h / 2 + y), - (w / 2 + x, -h / 2 + y), - (w / 2 + x, h / 2 + y), - (-w / 2 + x, h / 2 + y), - ) - def update_spatial_hash(self) -> None: """ Update the sprites location in the spatial hash if present. @@ -570,7 +556,7 @@ def draw_hit_box(self, color: Color = BLACK, line_thickness: float = 2.0) -> Non :param color: Color of box :param line_thickness: How thick the box should be """ - points = self.get_adjusted_hit_box() + points = self.hit_box.get_adjusted_points() # NOTE: This is a COPY operation. We don't want to modify the points. points = tuple(points) + tuple(points[:-1]) arcade.draw_line_strip(points, color=color, line_width=line_thickness) @@ -594,7 +580,7 @@ def collides_with_point(self, point: Point) -> bool: from arcade.geometry import is_point_in_polygon x, y = point - return is_point_in_polygon(x, y, self.get_adjusted_hit_box()) + return is_point_in_polygon(x, y, self.hit_box.get_adjusted_points()) def collides_with_sprite(self: SpriteType, other: SpriteType) -> bool: """Will check if a sprite is overlapping (colliding) another Sprite. From 21e80fe58433595f5bfdb925ee587269a19d949f Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sat, 18 Mar 2023 02:24:19 -0400 Subject: [PATCH 09/10] HitBox API updates --- .../platform_tutorial/12_animate_character.py | 14 +- .../platform_tutorial/13_add_enemies.py | 14 +- .../platform_tutorial/14_moving_enemies.py | 14 +- .../15_collision_with_enemies.py | 13 +- .../platform_tutorial/16_shooting_bullets.py | 13 +- arcade/examples/platform_tutorial/17_views.py | 14 +- arcade/hitbox/__init__.py | 4 +- arcade/hitbox/base.py | 96 +++++++------- arcade/sprite/base.py | 88 +++++++------ arcade/sprite/colored.py | 36 +++-- arcade/sprite/sprite.py | 123 ++---------------- arcade/texture/texture.py | 103 ++++++++------- arcade/tilemap/tilemap.py | 4 +- tests/unit/texture/test_textures.py | 9 +- 14 files changed, 265 insertions(+), 280 deletions(-) diff --git a/arcade/examples/platform_tutorial/12_animate_character.py b/arcade/examples/platform_tutorial/12_animate_character.py index 937d1946d..08b523ae2 100644 --- a/arcade/examples/platform_tutorial/12_animate_character.py +++ b/arcade/examples/platform_tutorial/12_animate_character.py @@ -93,9 +93,17 @@ def __init__(self): self.texture = self.idle_texture_pair[0] # Hit box will be set based on the first image used. If you want to specify - # a different hit box, you can do it like the code below. - # set_hit_box = [[-22, -64], [22, -64], [22, 28], [-22, 28]] - self.hit_box = self.texture.hit_box.adjustable(self) + # a different hit box, you can do it like the code below. Doing this when + # changing the texture for example would make the hitbox update whenever the + # texture is changed. This can be expensive so if the textures are very similar + # it may not be worth doing. + # + # self.hit_box = arcade.hitbox.RotatableHitBox( + # self.texture.hit_box_points, + # position=self.position, + # scale=self.scale_xy, + # angle=self.angle, + # ) def update_animation(self, delta_time: float = 1 / 60): # Figure out if we need to flip face left or right diff --git a/arcade/examples/platform_tutorial/13_add_enemies.py b/arcade/examples/platform_tutorial/13_add_enemies.py index 5176a363d..f4952157f 100644 --- a/arcade/examples/platform_tutorial/13_add_enemies.py +++ b/arcade/examples/platform_tutorial/13_add_enemies.py @@ -85,9 +85,17 @@ def __init__(self, name_folder, name_file): self.texture = self.idle_texture_pair[0] # Hit box will be set based on the first image used. If you want to specify - # a different hit box, you can do it like the code below. - # set_hit_box = [[-22, -64], [22, -64], [22, 28], [-22, 28]] - self.hit_box = self.texture.hit_box.adjustable(self) + # a different hit box, you can do it like the code below. Doing this when + # changing the texture for example would make the hitbox update whenever the + # texture is changed. This can be expensive so if the textures are very similar + # it may not be worth doing. + # + # self.hit_box = arcade.hitbox.RotatableHitBox( + # self.texture.hit_box_points, + # position=self.position, + # scale=self.scale_xy, + # angle=self.angle, + # ) class Enemy(Entity): diff --git a/arcade/examples/platform_tutorial/14_moving_enemies.py b/arcade/examples/platform_tutorial/14_moving_enemies.py index c66a381ad..3312bc15f 100644 --- a/arcade/examples/platform_tutorial/14_moving_enemies.py +++ b/arcade/examples/platform_tutorial/14_moving_enemies.py @@ -91,9 +91,17 @@ def __init__(self, name_folder, name_file): self.texture = self.idle_texture_pair[0] # Hit box will be set based on the first image used. If you want to specify - # a different hit box, you can do it like the code below. - # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]]) - self.hit_box = self.texture.hit_box.adjustable(self) + # a different hit box, you can do it like the code below. Doing this when + # changing the texture for example would make the hitbox update whenever the + # texture is changed. This can be expensive so if the textures are very similar + # it may not be worth doing. + # + # self.hit_box = arcade.hitbox.RotatableHitBox( + # self.texture.hit_box_points, + # position=self.position, + # scale=self.scale_xy, + # angle=self.angle, + # ) class Enemy(Entity): diff --git a/arcade/examples/platform_tutorial/15_collision_with_enemies.py b/arcade/examples/platform_tutorial/15_collision_with_enemies.py index 7fc7cf912..5eea8deaf 100644 --- a/arcade/examples/platform_tutorial/15_collision_with_enemies.py +++ b/arcade/examples/platform_tutorial/15_collision_with_enemies.py @@ -91,8 +91,17 @@ def __init__(self, name_folder, name_file): self.texture = self.idle_texture_pair[0] # Hit box will be set based on the first image used. If you want to specify - # a different hit box, you can do it like the code below. - self.hit_box = self.texture.hit_box.adjustable(self) + # a different hit box, you can do it like the code below. Doing this when + # changing the texture for example would make the hitbox update whenever the + # texture is changed. This can be expensive so if the textures are very similar + # it may not be worth doing. + # + # self.hit_box = arcade.hitbox.RotatableHitBox( + # self.texture.hit_box_points, + # position=self.position, + # scale=self.scale_xy, + # angle=self.angle, + # ) class Enemy(Entity): diff --git a/arcade/examples/platform_tutorial/16_shooting_bullets.py b/arcade/examples/platform_tutorial/16_shooting_bullets.py index a41953c0e..1caab59d5 100644 --- a/arcade/examples/platform_tutorial/16_shooting_bullets.py +++ b/arcade/examples/platform_tutorial/16_shooting_bullets.py @@ -98,8 +98,17 @@ def __init__(self, name_folder, name_file): self.texture = self.idle_texture_pair[0] # Hit box will be set based on the first image used. If you want to specify - # a different hit box, you can do it like the code below. - self.hit_box = self.texture.hit_box.adjustable(self) + # a different hit box, you can do it like the code below. Doing this when + # changing the texture for example would make the hitbox update whenever the + # texture is changed. This can be expensive so if the textures are very similar + # it may not be worth doing. + # + # self.hit_box = arcade.hitbox.RotatableHitBox( + # self.texture.hit_box_points, + # position=self.position, + # scale=self.scale_xy, + # angle=self.angle, + # ) class Enemy(Entity): diff --git a/arcade/examples/platform_tutorial/17_views.py b/arcade/examples/platform_tutorial/17_views.py index b9ac97f22..743eeb75c 100644 --- a/arcade/examples/platform_tutorial/17_views.py +++ b/arcade/examples/platform_tutorial/17_views.py @@ -96,9 +96,17 @@ def __init__(self, name_folder, name_file): self.texture = self.idle_texture_pair[0] # Hit box will be set based on the first image used. If you want to specify - # a different hit box, you can do it like the code below. - # self.set_hit_box([[-22, -64], [22, -64], [22, 28], [-22, 28]]) - self.hit_box = self.texture.hit_box.adjustable(self) + # a different hit box, you can do it like the code below. Doing this when + # changing the texture for example would make the hitbox update whenever the + # texture is changed. This can be expensive so if the textures are very similar + # it may not be worth doing. + # + # self.hit_box = arcade.hitbox.RotatableHitBox( + # self.texture.hit_box_points, + # position=self.position, + # scale=self.scale_xy, + # angle=self.angle, + # ) class Enemy(Entity): diff --git a/arcade/hitbox/__init__.py b/arcade/hitbox/__init__.py index 9460e5111..5bc884d1f 100644 --- a/arcade/hitbox/__init__.py +++ b/arcade/hitbox/__init__.py @@ -2,7 +2,7 @@ from arcade.types import PointList -from .base import AdjustableHitBox, HitBox, HitBoxAlgorithm +from .base import HitBox, HitBoxAlgorithm, RotatableHitBox from .bounding_box import BoundingHitBoxAlgorithm from .pymunk import PymunkHitBoxAlgorithm from .simple import SimpleHitBoxAlgorithm @@ -50,7 +50,7 @@ def calculate_hit_box_points_detailed( __all__ = [ "HitBoxAlgorithm", "HitBox", - "AdjustableHitBox", + "RotatableHitBox", "SimpleHitBoxAlgorithm", "PymunkHitBoxAlgorithm", "BoundingHitBoxAlgorithm", diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index d12397cbb..eede09510 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -39,47 +39,14 @@ def __call__(self, *args: Any, **kwds: Any) -> "HitBoxAlgorithm": class HitBox: - def __init__(self, points: PointList): - self._points = points - - @property - def points(self): - return self._points - - def create_adjustable( - self, - position: Tuple[float, float] = (0.0, 0.0), - angle: float = 0.0, - scale: Tuple[float, float] = (1.0, 1.0), - ) -> AdjustableHitBox: - return AdjustableHitBox( - self._points, position=position, angle=angle, scale=scale - ) - - def adjustable(self, sprite: Sprite) -> AdjustableHitBox: - return AdjustableHitBox( - self._points, - position=sprite.position, - angle=sprite.angle, - scale=sprite.scale_xy, - ) - - def get_adjusted_points(self): - return self.points - - -class AdjustableHitBox(HitBox): def __init__( self, points: PointList, - *, - position: Tuple[float, float] = (0.0, 0.0), - angle: float = 0.0, + position: Point = (0.0, 0.0), scale: Tuple[float, float] = (1.0, 1.0), ): - super().__init__(points) + self._points = points self._position = position - self._angle = angle self._scale = scale self._left = None @@ -90,6 +57,10 @@ def __init__( self._adjusted_points = None self._adjusted_cache_dirty = True + @property + def points(self): + return self._points + @property def position(self): return self._position @@ -123,15 +94,6 @@ def bottom(self): y_points = [point[1] for point in points] return min(y_points) - @property - def angle(self): - return self._angle - - @angle.setter - def angle(self, angle: float): - self._angle = angle - self._adjusted_cache_dirty = True - @property def scale(self): return self._scale @@ -141,6 +103,52 @@ def scale(self, scale: Tuple[float, float]): self._scale = scale self._adjusted_cache_dirty = True + def create_rotatable( + self, + angle: float = 0.0, + ) -> RotatableHitBox: + return RotatableHitBox( + self._points, position=self._position, scale=self._scale, angle=angle + ) + + def get_adjusted_points(self): + if not self._adjusted_cache_dirty: + return self._adjusted_points + + def _adjust_point(point) -> Point: + x, y = point + + x *= self.scale[0] + y *= self.scale[1] + + return (x + self.position[0], y + self.position[1]) + + self._adjusted_points = tuple([_adjust_point(point) for point in self.points]) + self._adjusted_cache_dirty = False + return self._adjusted_points + + +class RotatableHitBox(HitBox): + def __init__( + self, + points: PointList, + *, + position: Tuple[float, float] = (0.0, 0.0), + angle: float = 0.0, + scale: Tuple[float, float] = (1.0, 1.0), + ): + super().__init__(points, position=position, scale=scale) + self._angle = angle + + @property + def angle(self): + return self._angle + + @angle.setter + def angle(self, angle: float): + self._angle = angle + self._adjusted_cache_dirty = True + def get_adjusted_points(self): if not self._adjusted_cache_dirty: return self._adjusted_points diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 6e3066e8d..34cd3fac0 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -26,6 +26,7 @@ class BasicSprite: "_scale", "_color", "_texture", + "_hit_box", "sprite_lists", "_angle", "__weakref__", @@ -51,9 +52,14 @@ def __init__( # Core properties we don't use, but spritelist expects it self._angle = 0.0 + self._hit_box = HitBox( + self._texture.hit_box_points, self._position, self._scale + ) + # --- Core Properties --- - def _get_position(self) -> Point: + @property + def position(self) -> Point: """ Get or set the center x and y position of the sprite. @@ -62,49 +68,41 @@ def _get_position(self) -> Point: """ return self._position - def _set_position(self, new_value: Point): + @position.setter + def position(self, new_value: Point): if new_value == self._position: return self._position = new_value + self._hit_box.position = new_value self.update_spatial_hash() for sprite_list in self.sprite_lists: sprite_list._update_position(self) - position = property(_get_position, _set_position) - - def _get_center_x(self) -> float: + @property + def center_x(self) -> float: """Get or set the center x position of the sprite.""" return self._position[0] - def _set_center_x(self, new_value: float): + @center_x.setter + def center_x(self, new_value: float): if new_value == self._position[0]: return - self._position = (new_value, self._position[1]) - self.update_spatial_hash() - - for sprite_list in self.sprite_lists: - sprite_list._update_position_x(self) + self.position = (new_value, self._position[1]) - center_x = property(_get_center_x, _set_center_x) - - def _get_center_y(self) -> float: + @property + def center_y(self) -> float: """Get or set the center y position of the sprite.""" return self._position[1] - def _set_center_y(self, new_value: float): + @center_y.setter + def center_y(self, new_value: float): if new_value == self._position[1]: return - self._position = (self._position[0], new_value) - self.update_spatial_hash() - - for sprite_list in self.sprite_lists: - sprite_list._update_position_y(self) - - center_y = property(_get_center_y, _set_center_y) + self.position = (self._position[0], new_value) @property def depth(self) -> float: @@ -124,36 +122,38 @@ def depth(self, new_value: float): for sprite_list in self.sprite_lists: sprite_list._update_depth(self) - def _get_width(self) -> float: + @property + def width(self) -> float: """Get or set width or the sprite in pixels""" return self._width - def _set_width(self, new_value: float): + @width.setter + def width(self, new_value: float): if new_value != self._width: self._scale = new_value / self._texture.width, self._scale[1] + self._hit_box.scale = self._scale self._width = new_value self.update_spatial_hash() for sprite_list in self.sprite_lists: sprite_list._update_width(self) - width = property(_get_width, _set_width) - - def _get_height(self) -> float: + @property + def height(self) -> float: """Get or set the height of the sprite in pixels.""" return self._height - def _set_height(self, new_value: float): + @height.setter + def height(self, new_value: float): if new_value != self._height: self._scale = self._scale[0], new_value / self._texture.height + self._hit_box.scale = self._scale self._height = new_value self.update_spatial_hash() for sprite_list in self.sprite_lists: sprite_list._update_height(self) - height = property(_get_height, _set_height) - # @property # def size(self) -> Point: # """Get or set the size of the sprite as a pair of values.""" @@ -170,7 +170,8 @@ def _set_height(self, new_value: float): # for sprite_list in self.sprite_lists: # sprite_list._update_size(self) - def _get_scale(self) -> float: + @property + def scale(self) -> float: """ Get or set the sprite's x scale value or set both x & y scale to the same value. @@ -179,11 +180,13 @@ def _get_scale(self) -> float: """ return self._scale[0] - def _set_scale(self, new_value: float): + @scale.setter + def scale(self, new_value: float): if new_value == self._scale[0] and new_value == self._scale[1]: return self._scale = new_value, new_value + self._hit_box.scale = self._scale if self._texture: self._width = self._texture.width * self._scale[0] self._height = self._texture.height * self._scale[1] @@ -192,8 +195,6 @@ def _set_scale(self, new_value: float): for sprite_list in self.sprite_lists: sprite_list._update_size(self) - scale = property(_get_scale, _set_scale) - @property def scale_xy(self) -> Point: """Get or set the x & y scale of the sprite as a pair of values.""" @@ -205,6 +206,7 @@ def scale_xy(self, new_value: Point): return self._scale = new_value + self._hit_box.scale = self._scale if self._texture: self._width = self._texture.width * self._scale[0] self._height = self._texture.height * self._scale[1] @@ -222,7 +224,7 @@ def left(self) -> float: When setting this property the sprite is positioned relative to the leftmost x coordinate in the hit box. """ - return self.center_x - self.width / 2 + return self._hit_box.left @left.setter def left(self, amount: float): @@ -238,7 +240,7 @@ def right(self) -> float: When setting this property the sprite is positioned relative to the rightmost x coordinate in the hit box. """ - return self.center_x + self.width / 2 + return self._hit_box.right @right.setter def right(self, amount: float): @@ -254,7 +256,7 @@ def bottom(self) -> float: When setting this property the sprite is positioned relative to the lowest y coordinate in the hit box. """ - return self._position[1] - self.height / 2 + return self._hit_box.bottom @bottom.setter def bottom(self, amount: float): @@ -270,7 +272,7 @@ def top(self) -> float: When setting this property the sprite is positioned relative to the highest y coordinate in the hit box. """ - return self._position[1] + self.height / 2 + return self._hit_box.top @top.setter def top(self, amount: float): @@ -440,7 +442,7 @@ def rescale_relative_to_point(self, point: Point, factor: float) -> None: return # set the scale and, if this sprite has a texture, the size data - self._scale = self._scale[0] * factor, self._scale[1] * factor + self.scale_xy = self._scale[0] * factor, self._scale[1] * factor if self._texture: self._width = self._texture.width * self._scale[0] self._height = self._texture.height * self._scale[1] @@ -450,7 +452,7 @@ def rescale_relative_to_point(self, point: Point, factor: float) -> None: # be lazy about math; only do it if we have to if position_changed: - self._position = ( + self.position = ( (self._position[0] - point[0]) * factor + point[0], (self._position[1] - point[1]) * factor + point[1], ) @@ -495,7 +497,7 @@ def rescale_xy_relative_to_point( return # set the scale and, if this sprite has a texture, the size data - self._scale = self._scale[0] * factor_x, self._scale[1] * factor_y + self.scale_xy = self._scale[0] * factor_x, self._scale[1] * factor_y if self._texture: self._width = self._texture.width * self._scale[0] self._height = self._texture.height * self._scale[1] @@ -505,7 +507,7 @@ def rescale_xy_relative_to_point( # be lazy about math; only do it if we have to if position_changed: - self._position = ( + self.position = ( (self._position[0] - point[0]) * factor_x + point[0], (self._position[1] - point[1]) * factor_y + point[1], ) @@ -521,7 +523,7 @@ def rescale_xy_relative_to_point( @property def hit_box(self) -> HitBox: - return self._texture.hit_box + return self._hit_box def update_spatial_hash(self) -> None: """ diff --git a/arcade/sprite/colored.py b/arcade/sprite/colored.py index cb5c2487b..1d53ff4ef 100644 --- a/arcade/sprite/colored.py +++ b/arcade/sprite/colored.py @@ -3,8 +3,12 @@ import arcade from arcade import cache, hitbox from arcade.hitbox import HitBox -from arcade.texture import (ImageData, Texture, make_circle_texture, - make_soft_circle_texture) +from arcade.texture import ( + ImageData, + Texture, + make_circle_texture, + make_soft_circle_texture, +) from arcade.types import Color from .sprite import Sprite @@ -26,10 +30,11 @@ class SpriteSolidColor(Sprite): :param Color color: The color of the sprite as an RGB or RGBA tuple :param float angle: Initial angle of the sprite in degrees """ + __slots__ = () _default_image = ImageData( PIL.Image.new("RGBA", size=(32, 32), color=(255, 255, 255, 255)), - hash="sprite_solid_color" + hash="sprite_solid_color", ) def __init__( @@ -44,14 +49,12 @@ def __init__( ): texture = Texture( self._default_image, - hit_box=HitBox( - ( - (-width / 2, -height / 2), - (width / 2, -height / 2), - (width / 2, height / 2), - (-width / 2, height / 2) - ) - ) + hit_box_points=( + (-width / 2, -height / 2), + (width / 2, -height / 2), + (width / 2, height / 2), + (-width / 2, height / 2), + ), ) texture.size = width, height super().__init__( @@ -84,6 +87,7 @@ class SpriteCircle(Sprite): :param bool soft: If ``True``, the circle will fade from an opaque center to transparent edges. """ + def __init__(self, radius: int, color: Color, soft: bool = False, **kwargs): radius = int(radius) diameter = radius * 2 @@ -93,9 +97,13 @@ def __init__(self, radius: int, color: Color, soft: bool = False, **kwargs): # is applied in the shader through the sprite's color attribute. # determine the texture's cache name. if soft: - cache_name = cache.crate_str_from_values("circle_texture_soft", diameter, 255, 255, 255, 255) + cache_name = cache.crate_str_from_values( + "circle_texture_soft", diameter, 255, 255, 255, 255 + ) else: - cache_name = cache.crate_str_from_values("circle_texture", diameter, 255, 255, 255, 255) + cache_name = cache.crate_str_from_values( + "circle_texture", diameter, 255, 255, 255, 255 + ) # Get existing texture from cache if possible texture = cache.texture_cache.get_with_config(cache_name, hitbox.algo_simple) @@ -118,4 +126,4 @@ def __init__(self, radius: int, color: Color, soft: bool = False, **kwargs): # apply results to the new sprite super().__init__(texture) self.color = color_rgba - self._points = self.texture.hit_box.points + self._points = self.texture.hit_box_points diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index fbdeab121..129ffc2cb 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from arcade import Texture, load_texture -from arcade.hitbox import AdjustableHitBox, HitBox +from arcade.hitbox import HitBox, RotatableHitBox from arcade.math import get_angle_degrees from arcade.texture import get_default_texture from arcade.types import PathOrTexture, Point @@ -75,13 +75,7 @@ def __init__( ) PymunkMixin.__init__(self) - self._hit_box: AdjustableHitBox = self._texture.hit_box.create_adjustable( - position=self._position, - angle=self.angle, - scale=self._scale, - ) - - self.angle = angle + self._angle = angle # Movement self._velocity = 0.0, 0.0 self.change_angle: float = 0.0 @@ -104,107 +98,15 @@ def __init__( # Debug properties self.guid: Optional[str] = None + self._hit_box: RotatableHitBox = self._hit_box.create_rotatable( + angle=self._angle + ) + self._width = self._texture.width * scale self._height = self._texture.height * scale # --- Properties --- - @BasicSprite.center_x.setter - def center_x(self, new_value: float): - self._hit_box.position = (new_value, self._position[1]) - BasicSprite.center_x.fset(self, new_value) - - @BasicSprite.center_y.setter - def center_y(self, new_value: float): - self._hit_box.position = (self._position[0], new_value) - BasicSprite.center_y.fset(self, new_value) - - @BasicSprite.position.setter - def position(self, new_value: Point): - self._hit_box.position = new_value - BasicSprite.position.fset(self, new_value) - - @BasicSprite.scale.setter - def scale(self, new_value: float): - BasicSprite.scale.fset(self, new_value) - self._hit_box.scale = (new_value, new_value) - - @BasicSprite.width.setter - def width(self, new_value: float): - new_scale = new_value / self._texture.width, self._scale[0] - self._hit_box.scale = new_scale - BasicSprite.width.fset(self, new_value) - - @BasicSprite.height.setter - def height(self, new_value: float): - new_scale = new_value / self._texture.height, self._scale[1] - self._hit_box.scale = new_scale - BasicSprite.height.fset(self, new_value) - - @property - def left(self) -> float: - """ - The leftmost x coordinate in the hit box. - - When setting this property the sprite is positioned - relative to the leftmost x coordinate in the hit box. - """ - return self._hit_box.left - - @left.setter - def left(self, amount: float): - leftmost = self.left - diff = amount - leftmost - self.center_x += diff - - @property - def right(self) -> float: - """ - The rightmost x coordinate in the hit box. - - When setting this property the sprite is positioned - relative to the rightmost x coordinate in the hit box. - """ - return self._hit_box.right - - @right.setter - def right(self, amount: float): - rightmost = self.right - diff = rightmost - amount - self.center_x -= diff - - @property - def bottom(self) -> float: - """ - The lowest y coordinate in the hit box. - - When setting this property the sprite is positioned - relative to the lowest y coordinate in the hit box. - """ - return self._hit_box.bottom - - @bottom.setter - def bottom(self, amount: float): - lowest = self.bottom - diff = lowest - amount - self.center_y -= diff - - @property - def top(self) -> float: - """ - The highest y coordinate in the hit box. - - When setting this property the sprite is positioned - relative to the highest y coordinate in the hit box. - """ - return self._hit_box.top - - @top.setter - def top(self, amount: float): - highest = self.top - diff = highest - amount - self.center_y -= diff - @property def angle(self) -> float: """ @@ -287,11 +189,9 @@ def hit_box(self) -> HitBox: return self._hit_box @hit_box.setter - def hit_box(self, hit_box: Union[HitBox, AdjustableHitBox]): + def hit_box(self, hit_box: Union[HitBox, RotatableHitBox]): if type(hit_box) == HitBox: - self._hit_box = hit_box.create_adjustable( - self.position, self.angle, self.scale_xy - ) + self._hit_box = hit_box.create_rotatable(self.angle) else: # Mypy doesn't seem to understand the type check above # It still thinks hit_box can be a union here @@ -315,8 +215,11 @@ def texture(self, texture: Texture): # If sprite is using default texture, update the hit box if self._texture is get_default_texture(): - self.hit_box = texture.hit_box.create_adjustable( - position=self._position, angle=self.angle, scale=self._scale + self.hit_box = RotatableHitBox( + texture.hit_box_points, + position=self._position, + angle=self.angle, + scale=self._scale, ) self._texture = texture diff --git a/arcade/texture/texture.py b/arcade/texture/texture.py index 6d0b55221..48cba6f38 100644 --- a/arcade/texture/texture.py +++ b/arcade/texture/texture.py @@ -1,33 +1,33 @@ -import logging import hashlib -from typing import Any, Dict, Optional, Tuple, Type, Union, TYPE_CHECKING +import logging from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Type, Union from weakref import WeakSet import PIL.Image -import PIL.ImageOps import PIL.ImageDraw +import PIL.ImageOps -from arcade.types import Color +from arcade import cache as _cache +from arcade import hitbox +from arcade.color import TRANSPARENT_BLACK +from arcade.hitbox.base import HitBoxAlgorithm from arcade.texture.transforms import ( - Transform, + ORIENTATIONS, FlipLeftRightTransform, FlipTopBottomTransform, Rotate90Transform, Rotate180Transform, Rotate270Transform, + Transform, TransposeTransform, TransverseTransform, - ORIENTATIONS ) -from arcade.color import TRANSPARENT_BLACK -from arcade.hitbox.base import HitBoxAlgorithm, HitBox -from arcade import cache as _cache -from arcade import hitbox +from arcade.types import Color, PointList if TYPE_CHECKING: - from arcade.sprite_list import SpriteList from arcade import TextureAtlas + from arcade.sprite_list import SpriteList LOG = logging.getLogger(__name__) @@ -47,6 +47,7 @@ class ImageData: :param PIL.Image.Image image: The image for this texture :param str hash: The hash of the image """ + __slots__ = ("image", "hash", "__weakref__") hash_func = "sha256" @@ -119,11 +120,12 @@ class Texture: :param PIL.Image.Image image: The image or ImageData for this texture :param str hit_box_algorithm: The algorithm to use for calculating the hit box. - :param HitBox hit_box: A HitBox for the texture to use (Optional). + :param HitBox hit_box_points: A list of hitbox points for the texture to use (Optional). Completely overrides the hit box algorithm. :param str hash: Optional unique name for the texture. Can be used to make this texture globally unique. By default the hash of the pixel data is used. """ + __slots__ = ( "_image_data", "_size", @@ -131,7 +133,7 @@ class Texture: "_transforms", "_sprite_list", "_hit_box_algorithm", - "_hit_box", + "_hit_box_points", "_hash", "_cache_name", "_atlas_name", @@ -141,12 +143,13 @@ class Texture: "_atlas_refs", "__weakref__", ) + def __init__( self, image: Union[PIL.Image.Image, ImageData], *, hit_box_algorithm: Optional[HitBoxAlgorithm] = None, - hit_box: Optional[HitBox] = None, + hit_box_points: Optional[PointList] = None, hash: Optional[str] = None, **kwargs, ): @@ -183,7 +186,9 @@ def __init__( self._cache_name: str = "" self._atlas_name: str = "" self._update_cache_names() - self._hit_box: HitBox = hit_box or self._calculate_hit_box() + self._hit_box_points: PointList = ( + hit_box_points or self._calculate_hit_box_points() + ) # Track what atlases the image is in self._atlas_refs: Optional[WeakSet["TextureAtlas"]] = None @@ -208,7 +213,7 @@ def cache_name(self) -> str: """ The name of the texture used for caching (read only). - :return: str + :return: str """ return self._cache_name @@ -234,12 +239,12 @@ def create_cache_name( if not isinstance(hit_box_algorithm, HitBoxAlgorithm): raise TypeError(f"Expected HitBoxAlgorithm, got {type(hit_box_algorithm)}") - return ( - f"{hash}|{vertex_order}|{hit_box_algorithm.name}|{hit_box_algorithm.param_str}" - ) + return f"{hash}|{vertex_order}|{hit_box_algorithm.name}|{hit_box_algorithm.param_str}" @classmethod - def create_atlas_name(cls, hash: str, vertex_order: Tuple[int, int, int, int] = (0, 1, 2, 3)): + def create_atlas_name( + cls, hash: str, vertex_order: Tuple[int, int, int, int] = (0, 1, 2, 3) + ): return f"{hash}|{vertex_order}" def _update_cache_names(self): @@ -258,9 +263,7 @@ def _update_cache_names(self): @classmethod def create_image_cache_name( - cls, - path: Union[str, Path], - crop: Tuple[int, int, int, int] = (0, 0, 0, 0) + cls, path: Union[str, Path], crop: Tuple[int, int, int, int] = (0, 0, 0, 0) ): return f"{str(path)}|{crop}" @@ -269,7 +272,7 @@ def atlas_name(self) -> str: """ The name of the texture used for the texture atlas (read only). - :return: str + :return: str """ return self._atlas_name @@ -332,7 +335,7 @@ def image_data(self) -> ImageData: to determine the uniqueness of the image in texture atlases. - :return: ImageData + :return: ImageData """ return self._image_data @@ -387,7 +390,7 @@ def size(self, value: Tuple[int, int]): self._size = value @property - def hit_box(self) -> HitBox: + def hit_box_points(self) -> PointList: """ Get the hit box points for this texture. @@ -396,7 +399,7 @@ def hit_box(self) -> HitBox: :return: PointList """ - return self._hit_box + return self._hit_box_points @property def hit_box_algorithm(self) -> HitBoxAlgorithm: @@ -413,7 +416,7 @@ def create_filled(cls, name: str, size: Tuple[int, int], color: Color) -> "Textu :param str name: Name of the texture :param Tuple[int, int] size: Size of the texture :param Color color: Color of the texture - :return: Texture + :return: Texture """ return cls.create_empty(name, size, color) @@ -492,7 +495,7 @@ def flip_left_right(self) -> "Texture": has updated hit box data and a transform that will be applied to the image when it's drawn (GPU side). - :return: Texture + :return: Texture """ return self.transform(FlipLeftRightTransform) @@ -541,7 +544,7 @@ def flip_diagonally(self) -> "Texture": has updated hit box data and a transform that will be applied to the image when it's drawn (GPU side). - :return: Texture + :return: Texture """ return self.transpose() @@ -554,7 +557,7 @@ def transpose(self) -> "Texture": has updated hit box data and a transform that will be applied to the image when it's drawn (GPU side). - :return: Texture + :return: Texture """ return self.transform(TransposeTransform) @@ -567,7 +570,7 @@ def transverse(self) -> "Texture": has updated hit box data and a transform that will be applied to the image when it's drawn (GPU side). - :return: Texture + :return: Texture """ return self.transform(TransverseTransform) @@ -580,7 +583,7 @@ def rotate_90(self, count: int = 1) -> "Texture": applied to the image when it's drawn (GPU side). :param int count: Number of 90 degree steps to rotate. - :return: Texture + :return: Texture """ angles = [None, Rotate90Transform, Rotate180Transform, Rotate270Transform] count = count % 4 @@ -597,7 +600,7 @@ def rotate_180(self) -> "Texture": has updated hit box data and a transform that will be applied to the image when it's drawn (GPU side). - :return: Texture + :return: Texture """ return self.transform(Rotate180Transform) @@ -609,7 +612,7 @@ def rotate_270(self) -> "Texture": has updated hit box data and a transform that will be applied to the image when it's drawn (GPU side). - :return: Texture + :return: Texture """ return self.transform(Rotate270Transform) @@ -623,11 +626,11 @@ def transform( :param Transform transform: Transform to apply :return: New texture """ - new_hit_box = HitBox(transform.transform_hit_box_points(self._hit_box.points)) + new_hit_box_points = transform.transform_hit_box_points(self._hit_box_points) texture = Texture( self.image_data, hit_box_algorithm=self._hit_box_algorithm, - hit_box=new_hit_box, + hit_box_points=new_hit_box_points, hash=self._hash, ) texture.width = self.width @@ -666,10 +669,15 @@ def crop( :param int width: Width of crop :param int height: Height of crop :param bool cache: If True, the cropped texture will be cached - :return: Texture + :return: Texture """ # Return self if the crop is the same size as the original image - if (width == self.image.width and height == self.image.height and x == 0 and y == 0): + if ( + width == self.image.width + and height == self.image.height + and x == 0 + and y == 0 + ): return self # Return self width and height is 0 @@ -724,12 +732,16 @@ def remove_from_cache(self, ignore_error: bool = True) -> None: _cache.texture_cache.delete(self) @staticmethod - def validate_crop(image: PIL.Image.Image, x: int, y: int, width: int, height: int) -> None: + def validate_crop( + image: PIL.Image.Image, x: int, y: int, width: int, height: int + ) -> None: """ Validate the crop values for a given image. """ if x < 0 or y < 0 or width < 0 or height < 0: - raise ValueError(f"crop values must be positive: {x}, {y}, {width}, {height}") + raise ValueError( + f"crop values must be positive: {x}, {y}, {width}, {height}" + ) if x >= image.width: raise ValueError(f"x position is outside of texture: {x}") if y >= image.height: @@ -739,7 +751,7 @@ def validate_crop(image: PIL.Image.Image, x: int, y: int, width: int, height: in if y + height - 1 >= image.height: raise ValueError(f"height is outside of texture: {height + y}") - def _calculate_hit_box(self) -> HitBox: + def _calculate_hit_box_points(self) -> PointList: """ Calculate the hit box points for this texture based on the configured hit box algorithm. This is usually done on texture creation @@ -748,20 +760,21 @@ def _calculate_hit_box(self) -> HitBox: # Check if we have cached points points = _cache.hit_box_cache.get(self.cache_name) if points: - return HitBox(points) + return points # Calculate points with the selected algorithm points = self._hit_box_algorithm.calculate(self.image) if self._hit_box_algorithm.cache: _cache.hit_box_cache.put(self.cache_name, points) - return HitBox(points) + return points # ----- Drawing functions ----- def _create_cached_spritelist(self) -> "SpriteList": """Create or return the cached sprite list.""" from arcade.sprite_list import SpriteList + if self._sprite_list is None: self._sprite_list = SpriteList(capacity=1) return self._sprite_list @@ -790,6 +803,7 @@ def draw_sized( :param int alpha: Alpha value to draw texture """ from arcade import Sprite + spritelist = self._create_cached_spritelist() sprite = Sprite( self, @@ -829,6 +843,7 @@ def draw_scaled( :param int alpha: The transparency of the texture `(0-255)`. """ from arcade import Sprite + spritelist = self._create_cached_spritelist() sprite = Sprite( self, diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index d8cee417e..273b535c8 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -24,7 +24,7 @@ SpriteList, get_window, ) -from arcade.hitbox import AdjustableHitBox, HitBoxAlgorithm +from arcade.hitbox import HitBoxAlgorithm, RotatableHitBox from arcade.texture.loading import _load_tilemap_texture if TYPE_CHECKING: @@ -560,7 +560,7 @@ def _create_sprite_from_tile( for point in points: point[0], point[1] = point[1], point[0] - my_sprite.hit_box = AdjustableHitBox( + my_sprite.hit_box = RotatableHitBox( points, position=my_sprite.position, angle=my_sprite.angle, diff --git a/tests/unit/texture/test_textures.py b/tests/unit/texture/test_textures.py index 205366b0a..6ce66e6cb 100644 --- a/tests/unit/texture/test_textures.py +++ b/tests/unit/texture/test_textures.py @@ -1,10 +1,9 @@ -import pytest import PIL.Image import PIL.ImageDraw +import pytest import arcade -from arcade import Texture -from arcade import hitbox +from arcade import Texture, hitbox SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 @@ -65,7 +64,7 @@ def test_load_texture(): assert tex.width == 128 assert tex.height == 128 assert tex.size == (128, 128) - assert tex.hit_box is not None + assert tex.hit_box_points is not None assert tex._sprite_list is None with pytest.raises(FileNotFoundError): @@ -165,7 +164,7 @@ def test_crate_empty(): assert tex.crop_values is None assert tex.size == size assert tex._hit_box_algorithm == hitbox.algo_bounding_box - assert tex.hit_box.points == ( + assert tex.hit_box_points == ( (-128.0, -128.0), (128.0, -128.0), (128.0, 128.0), From 51148f821190839d79ed049f21f2b4c2d8acc2e0 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sat, 18 Mar 2023 02:26:48 -0400 Subject: [PATCH 10/10] Linting fixes --- arcade/hitbox/base.py | 5 +---- arcade/sprite/base.py | 2 +- arcade/sprite/colored.py | 1 - arcade/texture_atlas/helpers.py | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index eede09510..ffe21b295 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -1,15 +1,12 @@ from __future__ import annotations from math import cos, radians, sin -from typing import TYPE_CHECKING, Any, Tuple +from typing import Any, Tuple from PIL.Image import Image from arcade.types import Point, PointList -if TYPE_CHECKING: - from arcade.sprite import Sprite - class HitBoxAlgorithm: """ diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 34cd3fac0..9562c7745 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -4,7 +4,7 @@ from arcade.color import BLACK from arcade.hitbox import HitBox from arcade.texture import Texture -from arcade.types import RGBA, Color, Point, PointList +from arcade.types import RGBA, Color, Point if TYPE_CHECKING: from arcade.sprite_list import SpriteList diff --git a/arcade/sprite/colored.py b/arcade/sprite/colored.py index 1d53ff4ef..a42dc06ce 100644 --- a/arcade/sprite/colored.py +++ b/arcade/sprite/colored.py @@ -2,7 +2,6 @@ import arcade from arcade import cache, hitbox -from arcade.hitbox import HitBox from arcade.texture import ( ImageData, Texture, diff --git a/arcade/texture_atlas/helpers.py b/arcade/texture_atlas/helpers.py index cf9a33c08..d499da674 100644 --- a/arcade/texture_atlas/helpers.py +++ b/arcade/texture_atlas/helpers.py @@ -71,7 +71,7 @@ def save_atlas(atlas: TextureAtlas, directory: Path, name: str, resource_root: P "hash": texture.image_data.hash, "path": texture.file_path.relative_to(resource_root).as_posix(), "crop": texture.crop_values, - "points": texture.hit_box.points, + "points": texture.hit_box_points, "region": _dump_region_info(atlas.get_texture_region_info(texture.atlas_name)), "vertex_order": texture._vertex_order, })