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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
- Fixed `arcade.gui.experimental.pixelated_ui()` raising `AttributeError`. It now turns off text anti-aliasing and makes font glyphs use nearest-neighbor filtering, as documented.
- Fixed `InputManager.parse()` raising `AttributeError`, so a saved input configuration couldn't be loaded.

### Breaking Changes
- `arcade.math.get_angle_degrees()` now measures clockwise from straight up, like `Sprite.angle`: 0 is up, 90 is right, 180 is down and -90 is left. It used to measure clockwise from the right, which disagreed with `get_angle_radians()`, which already measured from up ([#2754](https://github.com/pythonarcade/arcade/issues/2754)). The two now always give the same angle in different units. A sprite whose image points up faces a point with `sprite.angle = get_angle_degrees(...)`. For the old value, subtract 90.
- Fixed the `sprite_rotate_around_tank` example's corrected barrel, which sat 90 degrees off from the mouse, and updated `turn_and_move` for the new angles. `turn_and_move`'s sprite also now stops when moving straight along an axis, which it never did.

### 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.
- Added the `sprite_pixel_demolition` example, a stress test where every pixel of the Arcade logo is a sprite (28,000, or 71,000 with smaller pixels) and exploding bullets knock them loose as debris. Debris can move as Python sprites or on the GPU with a shader (press G), which keeps 60 FPS with 15,000 pieces in flight.
Expand Down
9 changes: 5 additions & 4 deletions arcade/examples/sprite_rotate_around_tank.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,9 @@ def move_tank(self, delta_time):
self.tank.center_x, self.tank.center_y,
self.mouse_pos[0], self.mouse_pos[1])

# Compensate for fact that the barrel sits horzontally rather than virtically
mouse_angle -= 90
# At an angle of 0 the barrel points down from the tank's center,
# so turn it around to point at the mouse
mouse_angle -= 180

# Rotate the barrel sprite with one end at the tank's center
# Subtract the old angle to get the change in angle
Expand Down Expand Up @@ -209,8 +210,8 @@ def correct(self, correct: bool):
self._correct = correct
if correct:
angle = get_angle_radians(
self.tank.center_y, self.tank.center_x,
self.mouse_pos[1], self.mouse_pos[0])
self.tank.center_x, self.tank.center_y,
self.mouse_pos[0], self.mouse_pos[1])

self.barrel.position = (
self.barrel.center_x + math.sin(angle) * TANK_BARREL_LENGTH_HALF,
Expand Down
16 changes: 8 additions & 8 deletions arcade/examples/turn_and_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
WINDOW_HEIGHT = 720
WINDOW_TITLE = "Turn and Move Example"

# Image might not be lined up right, set this to offset
IMAGE_ROTATION = -90
# The image points down, so turn it around. Angles are measured from up.
IMAGE_ROTATION = -180


class Player(arcade.Sprite):
Expand Down Expand Up @@ -61,8 +61,8 @@ def update(self, delta_time: float = 1 / 60):
dest_y = self._destination_point[1]

# Do math to calculate how to get the sprite to the destination.
# Calculation the angle in radians between the start points
# and end points. This is the angle the player will travel.
# Calculate the angle in degrees from the start point to the
# end point, clockwise from up. This is the angle the player will travel.
target_angle = arcade.math.get_angle_degrees(start_x, start_y, dest_x, dest_y)
current_angle = self.angle - IMAGE_ROTATION

Expand All @@ -73,19 +73,19 @@ def update(self, delta_time: float = 1 / 60):
if angle_diff < 0.1 or 359.9 < angle_diff:
self.angle = target_angle + IMAGE_ROTATION
target_radians = math.radians(target_angle)
self.change_x = math.cos(-target_radians) * self.speed
self.change_y = math.sin(-target_radians) * self.speed
self.change_x = math.sin(target_radians) * self.speed
self.change_y = math.cos(target_radians) * self.speed

# Fine-tune our change_x/change_y if we are really close to destination
# point and just need to set to that location.
traveling = False
if abs(self.center_x - dest_x) < abs(self.change_x * delta_time):
if abs(self.center_x - dest_x) <= abs(self.change_x * delta_time):
self.center_x = dest_x
else:
self.center_x += self.change_x * delta_time
traveling = True

if abs(self.center_y - dest_y) < abs(self.change_y * delta_time):
if abs(self.center_y - dest_y) <= abs(self.change_y * delta_time):
self.center_y = dest_y
else:
self.center_y += self.change_y * delta_time
Expand Down
15 changes: 12 additions & 3 deletions arcade/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,12 @@ def rotate_around_point(source: Point2, target: Point2, angle: float):

def get_angle_degrees(x1: float, y1: float, x2: float, y2: float) -> float:
"""
Get the angle in degrees between two points.
Get the angle in degrees from the first point to the second.

The angle is measured clockwise from straight up, like
:py:attr:`Sprite.angle <arcade.Sprite.angle>`: 0 is up, 90 is right,
180 is down and -90 is left. A sprite whose image points up faces the
second point with ``sprite.angle = get_angle_degrees(...)``.

Args:
x1 (float): x coordinate of the first point
Expand All @@ -437,12 +442,16 @@ def get_angle_degrees(x1: float, y1: float, x2: float, y2: float) -> float:
"""
x_diff = x2 - x1
y_diff = y2 - y1
return -math.degrees(math.atan2(y_diff, x_diff))
return math.degrees(math.atan2(x_diff, y_diff))


def get_angle_radians(x1: float, y1: float, x2: float, y2: float) -> float:
"""
Get the angle in radians between two points.
Get the angle in radians from the first point to the second.

The same angle as :py:func:`get_angle_degrees`, in radians: measured
clockwise from straight up, like
:py:attr:`Sprite.radians <arcade.Sprite.radians>`.

Args:
x1 (float): x coordinate of the first point
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
import pytest
from pytest import approx

import arcade
from arcade.math import (
get_angle_degrees,
get_angle_radians,
lerp,
lerp_2d,
lerp_angle,
Expand Down Expand Up @@ -127,3 +130,42 @@ def test_rotate_around_point_180_reflects_through_source():
rx, ry = rotate_around_point(source, target, 180.0)
assert rx == approx(2.0 * source[0] - target[0])
assert ry == approx(2.0 * source[1] - target[1])


@pytest.mark.parametrize(
("x", "y", "expected"),
[
(0, 10, 0.0),
(10, 0, 90.0),
(0, -10, 180.0),
(-10, 0, -90.0),
(10, 10, 45.0),
(-10, -10, -135.0),
],
)
def test_get_angle_degrees_is_clockwise_from_up(x, y, expected):
# Measured from the first point, like Sprite.angle: 0 is up, 90 is right
assert get_angle_degrees(5, 7, 5 + x, 7 + y) == approx(expected)


@pytest.mark.parametrize(
("x1", "y1", "x2", "y2"),
[(0, 0, 10, 0), (0, 0, 0, 10), (3, 4, -2, 9), (-5, 1, -8, -6), (1, 1, 1, -1)],
)
def test_get_angle_radians_matches_degrees(x1, y1, x2, y2):
# The two functions give the same angle in different units (#2754)
assert get_angle_radians(x1, y1, x2, y2) == approx(
math.radians(get_angle_degrees(x1, y1, x2, y2))
)


def test_get_angle_degrees_points_an_up_facing_sprite(window):
# A sprite whose image points up, turned by get_angle_degrees, faces the point
sprite = arcade.SpriteSolidColor(10, 10, center_x=100, center_y=100)
for target in [(200, 100), (100, 300), (40, 20), (180, 190)]:
sprite.angle = get_angle_degrees(*sprite.position, *target)
# Sprite.angle turns clockwise, so straight up rotated by angle is (sin, cos)
facing = (math.sin(sprite.radians), math.cos(sprite.radians))
to_target = (target[0] - 100, target[1] - 100)
length = math.hypot(*to_target)
assert facing == approx((to_target[0] / length, to_target[1] / length))
Loading