From 195461c4c3d078bb38a9f5802edfd9c71b1d8f09 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 9 Feb 2023 22:59:52 -0500 Subject: [PATCH 01/13] Add benchmark script --- benchmarks/.gitignore | 1 + benchmarks/bench.sh | 51 +++++++++++++++++++++++ benchmarks/collisions/bench.py | 74 ++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 benchmarks/.gitignore create mode 100644 benchmarks/bench.sh create mode 100644 benchmarks/collisions/bench.py diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore new file mode 100644 index 000000000..a2e762ac2 --- /dev/null +++ b/benchmarks/.gitignore @@ -0,0 +1 @@ +/results.md \ No newline at end of file diff --git a/benchmarks/bench.sh b/benchmarks/bench.sh new file mode 100644 index 000000000..088f369cd --- /dev/null +++ b/benchmarks/bench.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# On windows, can run via the bash you get with git: +# C:\Program Files\Git\bin\bash.exe + +__dirname="$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$__dirname" +cd .. + +# I am doing this to ensure it runs against python 3.11, my default install is 3.10 +# You can probably remove this line +export PATH="/c/Users/cspot/AppData/Local/Programs/Python/Python311:$PATH" +python.exe --version + +# Get current branch name +gitTo=$( git rev-parse --abbrev-ref HEAD ) +# Get root of this branch +gitFrom=$( git merge-base origin/development $gitTo ) +gitCommitRange=$gitFrom..$gitTo + +commits=$(git log --format='%H' $gitCommitRange) +commits=$(echo "$commits" | sed -z 's/\n/,/g;s/,$/\n/') +echo "$commits" + +hyperfine \ + --export-markdown benchmarks/results.md \ + --warmup 1 --runs 2 \ + --parameter-list commit "$commits" \ + --setup 'git checkout {commit}' \ + 'python -m benchmarks.collisions.bench {commit}' + +git checkout $gitTo + +# Postprocess hyperfine's report to include commit messages and github links +python -c ' +import subprocess +import re + +report_path = "benchmarks/results.md" +with open(report_path,"r") as file: + report = file.read() + +def replace(x): + commit = x[1] + result = subprocess.run(["git", "log", "-n1", "--oneline", commit], capture_output=True, encoding="utf-8") + message = result.stdout.strip() + return f"[{message}](https://github.com/pythonarcade/arcade/commit/{commit})" +report = re.sub(r".*? ([a-f0-9]{40})`", replace, report) + +with open(report_path,"w") as file: + file.write(report) +' diff --git a/benchmarks/collisions/bench.py b/benchmarks/collisions/bench.py new file mode 100644 index 000000000..cfd5a6d4e --- /dev/null +++ b/benchmarks/collisions/bench.py @@ -0,0 +1,74 @@ +import math +import arcade +import pyglet +import random +import time + +SCREEN_WIDTH = 800 +SCREEN_HEIGHT = 600 + +WALL_DIM_MIN = 10 +WALL_DIM_MAX = 200 +WALLS_COUNT = 10 + +BULLET_VELOCITY_MIN = 1/60 +BULLET_VELOCITY_MAX = 10/60 +BULLET_COUNT = 1000 + +SIMULATE_MINUTES = 1 +SIMULATE_FPS = 60 + +# Predictable randomization so that each benchmark is identical +rng = random.Random(0) + +bullets = arcade.SpriteList() +walls = arcade.SpriteList() + +window = arcade.Window() + +# Seed chosen manually to create a wall distribution that looked good enough, +# like something I might create in a game. +rng.seed(2) +for i in range(0, WALLS_COUNT): + wall = arcade.SpriteSolidColor(rng.randint(WALL_DIM_MIN, WALL_DIM_MAX), rng.randint(WALL_DIM_MIN, WALL_DIM_MAX), arcade.color.BLACK) + wall.position = rng.randint(0, SCREEN_WIDTH), rng.randint(0, SCREEN_HEIGHT) + walls.append(wall) + +for i in range(0, BULLET_COUNT): + # Create a new bullet + new_bullet = arcade.SpriteCircle(color=arcade.color.RED, radius=10) + new_bullet.position = (rng.randint(0, SCREEN_WIDTH), rng.randint(0, SCREEN_HEIGHT)) + speed = rng.random() * (BULLET_VELOCITY_MAX - BULLET_VELOCITY_MIN) + BULLET_VELOCITY_MIN + angle = rng.random() * math.pi * 2 + new_bullet.velocity = (math.cos(angle) * speed, math.sin(angle) * speed) + # Half of bullets are rotated, to test those code paths + if rng.random() > 0.5: + new_bullet.angle = 45 + bullets.append(new_bullet) + +for i in range(0, int(SIMULATE_MINUTES * 60 * SIMULATE_FPS)): + pyglet.clock.tick() + + window.switch_to() + window.dispatch_events() + + # Move all bullets + for bullet in bullets: + bullet.position = (bullet.position[0] + bullet.velocity[0], bullet.position[1] + bullet.velocity[1]) + + # Check for collisions + bullets_w_collision = [] + for bullet in bullets: + walls_hit = arcade.check_for_collision_with_list(bullet, walls) + if walls_hit: + bullets_w_collision.append(bullet) + for bullet in bullets_w_collision: + # bullets.remove(bullet) + bullet.position = (rng.randint(0, SCREEN_WIDTH), rng.randint(0, SCREEN_HEIGHT)) + + window.dispatch_event('on_draw') + + window.clear(color=arcade.color.WHITE) + walls.draw() + bullets.draw() + window.flip() From 303c50568cb140f3548797ec1fd795c1144d0769 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 9 Feb 2023 23:08:29 -0500 Subject: [PATCH 02/13] get_adjusted_hit_box: avoid self. attr lookup in _adjust_point closure --- arcade/sprite.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/arcade/sprite.py b/arcade/sprite.py index c4acd77bb..51376938c 100644 --- a/arcade/sprite.py +++ b/arcade/sprite.py @@ -397,20 +397,23 @@ def get_adjusted_hit_box(self) -> PointList: if self._point_list_cache is not None: return self._point_list_cache + _angle = self._angle + _scale_x, _scale_y = self._scale + _position_x, _position_y = self._position def _adjust_point(point) -> Point: # Rotate the point if needed - if self._angle: + if _angle: # Rotate with scaling to not distort it if scale x and y is different - point = rotate_point(point[0] * self._scale[0], point[1] * self._scale[1], 0, 0, self._angle) + point = rotate_point(point[0] * _scale_x, point[1] * _scale_y, 0, 0, _angle) # Apply position return ( - point[0] + self._position[0], - point[1] + self._position[1], + point[0] + _position_x, + point[1] + _position_y, ) # Apply position and scale return ( - point[0] * self._scale[0] + self._position[0], - point[1] * self._scale[1] + self._position[1], + point[0] * _scale_x + _position_x, + point[1] * _scale_y + _position_y, ) # Cache the results From f76be2f29d2945c7d147987b606fae258bfb5908 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 9 Feb 2023 23:31:19 -0500 Subject: [PATCH 03/13] get_adjusted_hit_box: inline rotate_point() call --- arcade/sprite.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/arcade/sprite.py b/arcade/sprite.py index 51376938c..8e6a27d5d 100644 --- a/arcade/sprite.py +++ b/arcade/sprite.py @@ -397,23 +397,34 @@ def get_adjusted_hit_box(self) -> PointList: if self._point_list_cache is not None: return self._point_list_cache - _angle = self._angle + angle_radians = math.radians(self._angle) _scale_x, _scale_y = self._scale _position_x, _position_y = self._position + cos_angle = math.cos(angle_radians) + sin_angle = math.sin(angle_radians) def _adjust_point(point) -> Point: + x, y = point + x *= _scale_x + y *= _scale_y # Rotate the point if needed - if _angle: + if angle_radians: # Rotate with scaling to not distort it if scale x and y is different - point = rotate_point(point[0] * _scale_x, point[1] * _scale_y, 0, 0, _angle) + rotated_x = x * cos_angle - y * sin_angle + rotated_y = x * sin_angle + y * cos_angle + + # translate back + rounding_precision = 2 + x = round(rotated_x, rounding_precision) + y = round(rotated_y, rounding_precision) # Apply position return ( - point[0] + _position_x, - point[1] + _position_y, + x + _position_x, + y + _position_y, ) # Apply position and scale return ( - point[0] * _scale_x + _position_x, - point[1] * _scale_y + _position_y, + x + _position_x, + y + _position_y, ) # Cache the results From 7fb539395dc90078496aed46c3d958a73755199d Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 9 Feb 2023 23:34:35 -0500 Subject: [PATCH 04/13] get_adjusted_hit_box: use positional-only params in local function --- arcade/sprite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite.py b/arcade/sprite.py index 8e6a27d5d..a0dffcde8 100644 --- a/arcade/sprite.py +++ b/arcade/sprite.py @@ -402,7 +402,7 @@ def get_adjusted_hit_box(self) -> PointList: _position_x, _position_y = self._position cos_angle = math.cos(angle_radians) sin_angle = math.sin(angle_radians) - def _adjust_point(point) -> Point: + def _adjust_point(point, /) -> Point: x, y = point x *= _scale_x y *= _scale_y From 694acb88a7f3052ebe4029b6d2884de22ba46da0 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 9 Feb 2023 23:44:01 -0500 Subject: [PATCH 05/13] are_polygons_intersecting: remove cast calls --- arcade/geometry_python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/geometry_python.py b/arcade/geometry_python.py index 485af475f..804e513e5 100644 --- a/arcade/geometry_python.py +++ b/arcade/geometry_python.py @@ -50,7 +50,7 @@ def are_polygons_intersecting(poly_a: PointList, if max_b is None or projected > max_b: max_b = projected - if cast(float, max_a) <= cast(float, min_b) or cast(float, max_b) <= cast(float, min_a): + if max_a <= min_b or max_b <= min_a: return False return True From 574c2c92e87e852d363b28b3c8b2f7c4e52b1f39 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 9 Feb 2023 23:48:12 -0500 Subject: [PATCH 06/13] are_polygons_intersecting: misc tweaks --- arcade/geometry_python.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/arcade/geometry_python.py b/arcade/geometry_python.py index 804e513e5..c4b1d40da 100644 --- a/arcade/geometry_python.py +++ b/arcade/geometry_python.py @@ -24,30 +24,34 @@ def are_polygons_intersecting(poly_a: PointList, for polygon in (poly_a, poly_b): - for i1 in range(len(polygon)): - i2 = (i1 + 1) % len(polygon) + len_polygon = len(polygon) + 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_x = projection_2[1] - projection_1[1] + normal_y = projection_1[0] - projection_2[0] - min_a, max_a, min_b, max_b = (None,) * 4 + min_a = 99999 + max_a = -99999 + min_b = 99999 + max_b = -99999 for poly in poly_a: - projected = normal[0] * poly[0] + normal[1] * poly[1] + projected = normal_x * poly[0] + normal_y * poly[1] - if min_a is None or projected < min_a: + if projected < min_a: min_a = projected - if max_a is None or projected > max_a: + if projected > max_a: max_a = projected for poly in poly_b: - projected = normal[0] * poly[0] + normal[1] * poly[1] + projected = normal_x * poly[0] + normal_y * poly[1] - if min_b is None or projected < min_b: + if projected < min_b: min_b = projected - if max_b is None or projected > max_b: + if projected > max_b: max_b = projected if max_a <= min_b or max_b <= min_a: From 537b2026346d39e537dcff901efc8f84a1f01897 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 10 Feb 2023 00:02:35 -0500 Subject: [PATCH 07/13] _check_for_collision: optimize broadphase --- arcade/sprite_list/spatial_hash.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/arcade/sprite_list/spatial_hash.py b/arcade/sprite_list/spatial_hash.py index ba1a7f9e2..c2dece567 100644 --- a/arcade/sprite_list/spatial_hash.py +++ b/arcade/sprite_list/spatial_hash.py @@ -203,17 +203,23 @@ def _check_for_collision(sprite1: Sprite, sprite2: Sprite) -> bool: :returns: True if sprites overlap. :rtype: bool """ - radius_sum = max(sprite1._width, sprite1._height) + max(sprite2._width, sprite2._height) + sprite1_position = sprite1._position + sprite1_width = sprite1._width + sprite1_height = sprite1._height + sprite2_position = sprite2._position + sprite2_width = sprite2._width + sprite2_height = sprite2._height + radius_sum = (sprite1_width if sprite1_width > sprite1_height else sprite1_height) + (sprite2_width if sprite2_width > sprite2_height else sprite2_height) # Multiply by half of the theoretical max diagonal length for an estimation of distance radius_sum *= 0.71 # 1.42 / 2 radius_sum_x2 = radius_sum * radius_sum - diff_x = sprite1._position[0] - sprite2._position[0] + diff_x = sprite1_position[0] - sprite2_position[0] diff_x2 = diff_x * diff_x if diff_x2 > radius_sum_x2: return False - diff_y = sprite1._position[1] - sprite2._position[1] + diff_y = sprite1_position[1] - sprite2_position[1] diff_y2 = diff_y * diff_y if diff_y2 > radius_sum_x2: return False From d1ca44d0fe933a88f162513fe26d7a67f988722b Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 10 Feb 2023 12:19:55 -0500 Subject: [PATCH 08/13] get_adjusted_hit_box: replace generator expression with list comprehension, even though is immediately converted to a tuple --- arcade/sprite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite.py b/arcade/sprite.py index a0dffcde8..8ef81b8a5 100644 --- a/arcade/sprite.py +++ b/arcade/sprite.py @@ -428,7 +428,7 @@ def _adjust_point(point, /) -> Point: ) # Cache the results - self._point_list_cache = tuple(_adjust_point(point) for point in self.hit_box) + self._point_list_cache = tuple([_adjust_point(point) for point in self.hit_box]) return self._point_list_cache def forward(self, speed: float = 1.0) -> None: From c2e8d482eb6b68a9267b278ed1975e58cfaaf4a0 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 10 Feb 2023 12:33:30 -0500 Subject: [PATCH 09/13] check_for_collision_with_list: micro-opt --- arcade/sprite_list/spatial_hash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite_list/spatial_hash.py b/arcade/sprite_list/spatial_hash.py index c2dece567..e78cfc75e 100644 --- a/arcade/sprite_list/spatial_hash.py +++ b/arcade/sprite_list/spatial_hash.py @@ -308,7 +308,7 @@ def check_for_collision_with_list( f"Parameter 2 is a {type(sprite_list)} instead of expected SpriteList." ) - if sprite_list.spatial_hash and (method == 1 or method == 0): + if (method == 1 or method == 0) and sprite_list.spatial_hash: # Spatial sprite_list_to_check = sprite_list.spatial_hash.get_objects_for_box(sprite) # checks_saved = len(sprite_list) - len(sprite_list_to_check) From c9a17ce4d24b89b2f06db0772d531fa921a10bcc Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 10 Feb 2023 13:07:49 -0500 Subject: [PATCH 10/13] sprite.py: avoid `math.` attribute access by importing individual items at top of module --- arcade/sprite.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/arcade/sprite.py b/arcade/sprite.py index 8ef81b8a5..e7f95ef6d 100644 --- a/arcade/sprite.py +++ b/arcade/sprite.py @@ -5,7 +5,7 @@ https://www.gamedev.net/articles/programming/general-and-gameplay-programming/spatial-hashing-r2697/ """ -import math +from math import cos, sin, radians, sqrt, pi import dataclasses from typing import ( Any, @@ -397,11 +397,11 @@ def get_adjusted_hit_box(self) -> PointList: if self._point_list_cache is not None: return self._point_list_cache - angle_radians = math.radians(self._angle) + angle_radians = radians(self._angle) _scale_x, _scale_y = self._scale _position_x, _position_y = self._position - cos_angle = math.cos(angle_radians) - sin_angle = math.sin(angle_radians) + cos_angle = cos(angle_radians) + sin_angle = sin(angle_radians) def _adjust_point(point, /) -> Point: x, y = point x *= _scale_x @@ -439,8 +439,8 @@ def forward(self, speed: float = 1.0) -> None: :param speed: speed factor """ - self.change_x += math.cos(self.radians) * speed - self.change_y += math.sin(self.radians) * speed + self.change_x += cos(self.radians) * speed + self.change_y += sin(self.radians) * speed def reverse(self, speed: float = 1.0) -> None: """ @@ -460,8 +460,8 @@ def strafe(self, speed: float = 1.0) -> None: :param speed: speed factor """ - self.change_x += -math.sin(self.radians) * speed - self.change_y += math.cos(self.radians) * speed + self.change_x += -sin(self.radians) * speed + self.change_y += cos(self.radians) * speed def turn_right(self, theta: float = 90.0) -> None: """ @@ -856,14 +856,14 @@ def radians(self) -> float: Converts the degrees representation of self.angle into radians. :return: float """ - return self._angle / 180.0 * math.pi + return self._angle / 180.0 * pi @radians.setter def radians(self, new_value: float): """ Converts a radian value into degrees and stores it into angle. """ - self.angle = new_value * 180.0 / math.pi + self.angle = new_value * 180.0 / pi @property def left(self) -> float: @@ -1290,7 +1290,7 @@ def update_animation(self, delta_time: float = 1 / 60) -> None: x2 = self.last_texture_change_center_x y1 = self.center_y y2 = self.last_texture_change_center_y - distance = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) + distance = sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) texture_list: List[Texture] = [] change_direction = False From 2e32c8f8fbff03729db3b43c525968e35e9a0165 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 10 Feb 2023 13:52:51 -0500 Subject: [PATCH 11/13] get_adjusted_hit_box: avoid rounding off coords after rotation --- arcade/sprite.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/arcade/sprite.py b/arcade/sprite.py index e7f95ef6d..755523d24 100644 --- a/arcade/sprite.py +++ b/arcade/sprite.py @@ -412,14 +412,10 @@ def _adjust_point(point, /) -> Point: rotated_x = x * cos_angle - y * sin_angle rotated_y = x * sin_angle + y * cos_angle - # translate back - rounding_precision = 2 - x = round(rotated_x, rounding_precision) - y = round(rotated_y, rounding_precision) # Apply position return ( - x + _position_x, - y + _position_y, + rotated_x + _position_x, + rotated_y + _position_y, ) # Apply position and scale return ( From 0c0d548029650c072cfdc9cde8e14a28cc820eed Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 9 Feb 2023 23:57:04 -0500 Subject: [PATCH 12/13] top/bottom/left/right: pass generator to min/max --- arcade/sprite.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/arcade/sprite.py b/arcade/sprite.py index 755523d24..549d0dc2c 100644 --- a/arcade/sprite.py +++ b/arcade/sprite.py @@ -518,8 +518,7 @@ def bottom(self) -> float: if len(points) == 0: return self.center_y - y_points = [point[1] for point in points] - return min(y_points) + return min(point[1] for point in points) @bottom.setter def bottom(self, amount: float): @@ -542,8 +541,7 @@ def top(self) -> float: if len(points) == 0: return self.center_y - y_points = [point[1] for point in points] - return max(y_points) + return max(point[1] for point in points) @top.setter def top(self, amount: float): @@ -873,8 +871,7 @@ def left(self) -> float: if len(points) == 0: return self.center_x - x_points = [point[0] for point in points] - return min(x_points) + return min(point[0] for point in points) @left.setter def left(self, amount: float): @@ -895,8 +892,7 @@ def right(self) -> float: if len(points) == 0: return self.center_x - x_points = [point[0] for point in points] - return max(x_points) + return max(point[0] for point in points) @right.setter def right(self, amount: float): From 50804348a56aa0d23292935807790603585e2ebb Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 10 Feb 2023 01:31:58 -0500 Subject: [PATCH 13/13] HACK for common hitboxes with parallel sides --- arcade/cache/hit_box.py | 6 +++--- arcade/geometry_python.py | 9 +++++---- arcade/hitbox/bounding.py | 4 ++-- arcade/hitbox/detailed.py | 2 +- arcade/hitbox/simple.py | 3 ++- arcade/sprite.py | 3 +++ arcade/sprite_list/spatial_hash.py | 2 +- arcade/texture.py | 14 +++++++++----- 8 files changed, 26 insertions(+), 17 deletions(-) diff --git a/arcade/cache/hit_box.py b/arcade/cache/hit_box.py index 9b21d66b9..6367ce4e8 100644 --- a/arcade/cache/hit_box.py +++ b/arcade/cache/hit_box.py @@ -33,7 +33,7 @@ class HitBoxCache: VERSION = 1 def __init__(self): - self._entries: OrderedDict[str, PointList] = OrderedDict() + self._entries: OrderedDict[str, (PointList, bool)] = OrderedDict() def __len__(self) -> int: return len(self._entries) @@ -57,7 +57,7 @@ def get(self, keys: List[Any]) -> Optional[PointList]: key = self._gen_key(*keys) return self._entries.get(key, None) - def put(self, keys: List[Any], points: PointList) -> None: + def put(self, keys: List[Any], points: PointList, opt = False) -> None: """ Store hit box points usually for a texture. @@ -78,7 +78,7 @@ def put(self, keys: List[Any], points: PointList) -> None: raise ValueError(f"Hit box must have at least 3 points: {points}") key = self._gen_key(*keys) - self._entries[key] = tuple(points) + self._entries[key] = (tuple(points), opt) def load(self, path: Union[str, Path]) -> None: """ diff --git a/arcade/geometry_python.py b/arcade/geometry_python.py index c4b1d40da..8637be00c 100644 --- a/arcade/geometry_python.py +++ b/arcade/geometry_python.py @@ -10,8 +10,8 @@ _PRECISION = 2 -def are_polygons_intersecting(poly_a: PointList, - poly_b: PointList) -> bool: +def are_polygons_intersecting(poly_a: PointList, poly_a_parallel: bool, + poly_b: PointList, poly_b_parallel: bool) -> bool: """ Return True if two polygons intersect. @@ -22,10 +22,11 @@ def are_polygons_intersecting(poly_a: PointList, :rtype bool: """ - for polygon in (poly_a, poly_b): + for (polygon, parallel) in ((poly_a, poly_a_parallel), (poly_b, poly_b_parallel)): len_polygon = len(polygon) - for i1 in range(len_polygon): + range_max = len_polygon // 2 if parallel else len_polygon + for i1 in range(range_max): i2 = (i1 + 1) % len_polygon projection_1 = polygon[i1] projection_2 = polygon[i2] diff --git a/arcade/hitbox/bounding.py b/arcade/hitbox/bounding.py index 7fb9e2eb6..a850f724d 100644 --- a/arcade/hitbox/bounding.py +++ b/arcade/hitbox/bounding.py @@ -19,9 +19,9 @@ def calculate(self, image: Image, **kwargs) -> PointList: :Returns: List of points """ size = image.size - return ( + return (( (-size[0] / 2, -size[1] / 2), (size[0] / 2, -size[1] / 2), (size[0] / 2, size[1] / 2), (-size[0] / 2, size[1] / 2), - ) + ), True) diff --git a/arcade/hitbox/detailed.py b/arcade/hitbox/detailed.py index c9d2d7315..57ce971fd 100644 --- a/arcade/hitbox/detailed.py +++ b/arcade/hitbox/detailed.py @@ -43,7 +43,7 @@ def calculate(self, image: Image, **kwargs) -> PointList: if len(line_set) > 4: line_set = simplify_curves(line_set, hit_box_detail) - return self.to_points_list(image, line_set) + return (self.to_points_list(image, line_set), False) def to_points_list(self, image: Image, line_set: List[Vec2d]) -> PointList: """ diff --git a/arcade/hitbox/simple.py b/arcade/hitbox/simple.py index 023c03ed3..f05f52150 100644 --- a/arcade/hitbox/simple.py +++ b/arcade/hitbox/simple.py @@ -93,4 +93,5 @@ def _r(point: Tuple[float, float], height: int, width: int) -> Point: result.append(_r(p8, h, w)) # Remove duplicates - return tuple(dict.fromkeys(result)) # type: ignore + points = tuple(dict.fromkeys(result)) # type: ignore + return (points, len(points) == 8) diff --git a/arcade/sprite.py b/arcade/sprite.py index 549d0dc2c..95edfe939 100644 --- a/arcade/sprite.py +++ b/arcade/sprite.py @@ -257,6 +257,7 @@ def __init__( if self._texture and not self._points: self._points = self._texture.hit_box_points + self._hit_box_parallel = self._texture._hit_box_parallel @property def properties(self) -> Dict[str, Any]: @@ -359,6 +360,7 @@ def get_hit_box(self) -> PointList: # If there is no hitbox, use the width/height to get one if self._points is None and self._texture: self._points = self._texture.hit_box_points + self._hit_box_parallel = self._texture._hit_box_parallel if self._points is None and self._width: x1, y1 = -self._width / 2, -self._height / 2 @@ -1482,6 +1484,7 @@ def __init__(self, radius: int, color: Color, soft: bool = False): self.texture = texture self.color = color_rgba self._points = self.texture.hit_box_points + self._hit_box_parallel = self.texture._hit_box_parallel def get_distance_between_sprites(sprite1: Sprite, sprite2: Sprite) -> float: diff --git a/arcade/sprite_list/spatial_hash.py b/arcade/sprite_list/spatial_hash.py index e78cfc75e..28e5ae8d3 100644 --- a/arcade/sprite_list/spatial_hash.py +++ b/arcade/sprite_list/spatial_hash.py @@ -229,7 +229,7 @@ def _check_for_collision(sprite1: Sprite, sprite2: Sprite) -> bool: return False return are_polygons_intersecting( - sprite1.get_adjusted_hit_box(), sprite2.get_adjusted_hit_box() + sprite1.get_adjusted_hit_box(), sprite1._hit_box_parallel, sprite2.get_adjusted_hit_box(), sprite2._hit_box_parallel ) diff --git a/arcade/texture.py b/arcade/texture.py index 0d6bad2b5..3ca911e57 100644 --- a/arcade/texture.py +++ b/arcade/texture.py @@ -156,7 +156,11 @@ def __init__( self._hit_box_algorithm = self._hit_box_algorithm.lower() self._hit_box_detail = hit_box_detail - self._hit_box_points: PointList = hit_box_points or self._calculate_hit_box_points() + self._hit_box_parallel = False + if hit_box_points: + self._hit_box_points = hit_box_points + else: + self._hit_box_points, self._hit_box_parallel = self._calculate_hit_box_points() @property def name(self) -> str: @@ -480,14 +484,14 @@ def _calculate_hit_box_points(self) -> PointList: # Check if we have cached points keys = [self._image_data.hash, algo_name, self._hit_box_detail] - points = self.hit_box_cache.get(keys) - if points: - return points + points_opt = self.hit_box_cache.get(keys) + if points_opt: + return points_opt # Calculate points with the selected algorithm algo = hitbox.get_algorithm(algo_name) points = algo.calculate(self.image, hit_box_algorithm=self._hit_box_detail) - self.hit_box_cache.put(keys, points) + self.hit_box_cache.put(keys, points[0], points[1]) return points