Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
- Fixed drawing on scaled (HiDPI) displays, such as Windows set to 125% or 150%:
- After the first frame that drew text, the whole game was drawn too small, in the bottom left corner of the window. pyglet sets the window camera's viewport and scissor when it draws text, and Arcade gave them in window units instead of pixels, so they also stayed wrong for everything drawn after. Text was too small even in the first frame, and GUI text was drawn at the wrong size, outside its widget.
- `Window.get_pixel_ratio()` returned pyglet's display scale instead of the framebuffer to window size ratio, which differ when pyglet's `dpi_scaling` option is `"platform"`. Everything on screen was then drawn scaled up, cut off at the right and top.
- Fixed setting `Sprite.hit_box`. The new hit box stayed at (0, 0) with a scale of 1 until the sprite moved or was scaled, so it collided with nothing, and a spatial hash didn't account for its size. It now takes the sprite's position, scale and angle right away.

### New Features
- Added `arcade.sweep_sprite(sprite, dx, dy, sprite_list)`, which checks the whole path of a moving sprite and returns a `SweepInfo` for the first sprite it would hit (the `sprite`, how far along the move as a `fraction` and `distance`, and the surface `normal`), or `None`. Fast sprites can't pass through thin walls this way. A sprite that already overlaps one is an immediate hit. Added the `sprite_bullets_sweep` example comparing it with a plain collision check.
Expand Down
22 changes: 15 additions & 7 deletions arcade/sprite/sprite.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,17 +221,25 @@ def change_y(self, new_value: float) -> None:

@property
def hit_box(self) -> HitBox:
"""Get or set the hit box for this sprite."""
"""
Get or set the hit box for this sprite.

The hit box's points are relative to the sprite's center, before
scaling and rotation. Setting it moves, scales and rotates the hit
box to match the sprite, so its own position, scale and angle
don't matter.
"""
return self._hit_box

@hit_box.setter
def hit_box(self, hit_box: HitBox | RotatableHitBox) -> None:
if type(hit_box) is HitBox:
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
self._hit_box = hit_box # type: ignore
if not isinstance(hit_box, RotatableHitBox):
hit_box = hit_box.create_rotatable(self.angle)
hit_box.position = self._position
hit_box.scale = self._scale
hit_box.angle = self.angle
self._hit_box = hit_box
self.update_spatial_hash()

@property
def texture(self) -> Texture:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/sprite/test_sprite_collision.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,7 @@ def test_sweep_sprite_ends_touching_slanted_edge(window):
sprite.hit_box = arcade.hitbox.HitBox([(5, 0), (0, 5), (-5, 0), (0, -5)])
wall = arcade.SpriteSolidColor(20, 20)
wall.hit_box = arcade.hitbox.HitBox([(10, 0), (0, 10), (-10, 0), (0, -10)])
# Set after the hit box, which is created at (0, 0)
# Moved after setting the hit box
wall.position = 30, 10
walls = _walls(wall)
# After moving 25, the sprite's upper right edge lies along the wall's
Expand Down
64 changes: 64 additions & 0 deletions tests/unit/sprite/test_sprite_hitbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,67 @@ def test_2():
(-8.0, -32.0),
(-28.0, -17.0),
]


SQUARE = ((-10, -10), (-10, 10), (10, 10), (10, -10))


def _rounded(points):
return [(round(x, 6), round(y, 6)) for x, y in points]


def test_set_hit_box_matches_sprite():
"""A new hit box takes the sprite's position, scale and angle right away"""
sprite = arcade.SpriteSolidColor(20, 20, center_x=100, center_y=50)
sprite.scale = 2
sprite.angle = 90
sprite.hit_box = hitbox.HitBox(SQUARE)

assert sprite.hit_box.position == (100, 50)
assert sprite.hit_box.scale == (2, 2)
assert sprite.hit_box.angle == 90
# Clockwise rotation by 90 degrees, then scaled by 2 and moved
assert _rounded(sprite.hit_box.get_adjusted_points()) == [
(80, 70), (120, 70), (120, 30), (80, 30)
] # fmt: skip


def test_set_rotatable_hit_box_matches_sprite():
"""A RotatableHitBox's own position, scale and angle are replaced too"""
sprite = arcade.SpriteSolidColor(20, 20, center_x=100, center_y=50)
sprite.scale = 0.5
sprite.hit_box = hitbox.RotatableHitBox(SQUARE, position=(7, 7), angle=30, scale=(3, 3))
assert sprite.hit_box.position == (100, 50)
assert sprite.hit_box.scale == (0.5, 0.5)
assert sprite.hit_box.angle == 0
assert _rounded(sprite.hit_box.get_adjusted_points()) == [
(95, 45), (95, 55), (105, 55), (105, 45)
] # fmt: skip


def test_set_hit_box_follows_sprite_afterwards():
sprite = arcade.SpriteSolidColor(20, 20, center_x=100, center_y=50)
sprite.hit_box = hitbox.HitBox(SQUARE)
sprite.position = 10, 20
sprite.scale = 3
assert _rounded(sprite.hit_box.get_adjusted_points()) == [
(-20, -10), (-20, 50), (40, 50), (40, -10)
] # fmt: skip


def test_set_hit_box_collides_right_away():
"""The hit box used to stay at (0, 0) until the sprite moved, so it hit nothing"""
sprite = arcade.SpriteSolidColor(20, 20, center_x=100, center_y=100)
sprite.hit_box = hitbox.HitBox(SQUARE)
other = arcade.SpriteSolidColor(20, 20, center_x=110, center_y=100)
assert arcade.check_for_collision(sprite, other)


def test_set_hit_box_updates_spatial_hash():
"""A bigger hit box is found by a spatial hash without moving the sprite"""
sprite_list = arcade.SpriteList(use_spatial_hash=True)
sprite = arcade.SpriteSolidColor(20, 20, center_x=100, center_y=100)
sprite_list.append(sprite)
sprite.hit_box = hitbox.HitBox([(-200, -200), (200, -200), (200, 200), (-200, 200)])
far_away = arcade.SpriteSolidColor(10, 10, center_x=250, center_y=250)
assert arcade.check_for_collision_with_list(far_away, sprite_list) == [sprite]
Loading