diff --git a/arcade/sprite.py b/arcade/sprite.py index c4acd77bb..8fb6f1f19 100644 --- a/arcade/sprite.py +++ b/arcade/sprite.py @@ -6,6 +6,7 @@ """ import math +from math import sin, cos, radians import dataclasses from typing import ( Any, @@ -22,7 +23,6 @@ from arcade.geometry_generic import get_angle_degrees from arcade import load_texture from arcade import Texture -from arcade import rotate_point from arcade import make_soft_circle_texture from arcade import make_circle_texture from arcade import Color @@ -397,24 +397,34 @@ def get_adjusted_hit_box(self) -> PointList: if self._point_list_cache is not None: return self._point_list_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 self._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) - # Apply position - return ( - point[0] + self._position[0], - point[1] + self._position[1], - ) - # Apply position and scale + 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 ( - point[0] * self._scale[0] + self._position[0], - point[1] * self._scale[1] + self._position[1], + x + position_x, + y + position_y, ) # 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: diff --git a/arcade/sprite_list/spatial_hash.py b/arcade/sprite_list/spatial_hash.py index ba1a7f9e2..5964f5336 100644 --- a/arcade/sprite_list/spatial_hash.py +++ b/arcade/sprite_list/spatial_hash.py @@ -203,17 +203,26 @@ 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 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..4e29adfb8 --- /dev/null +++ b/benchmarks/bench.sh @@ -0,0 +1,54 @@ +#!/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 .. + +bench_name="$1" + +# 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 )~1 +gitCommitRange=$gitFrom..$gitTo + +commits=$(git log --format='%H' $gitCommitRange) +commits=$(echo "$commits" | sed -z 's/\n/,/g;s/,$/\n/') +echo "$commits" + +hyperfine \ + --show-output \ + --export-markdown benchmarks/results.md \ + --warmup 1 --runs 2 \ + --parameter-list commit "$commits,$commits" \ + --setup 'git checkout {commit}' \ + 'python -m benchmarks.'"$bench_name"'.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()