Skip to content
Open
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
75 changes: 75 additions & 0 deletions doc/builtins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,81 @@ Actors or ``(x, y)`` coordinate pairs.
* Down is -90 degrees.


.. _angle_movement:

Angle Movement
''''''''''''''

If an actor is rotated and should move based on its rotation, doing so by
adjusting X and Y coordinates manually can be complicated. To make moving
actors around their rotation easier, Pygame Zero provides built-in functions.

.. method:: Actor.move_towards_angle(angle, distance)

Moves the actor the given distance along the given angle.


.. method:: Actor.move_towards_point(point, distance, [overshoot])

Moves the actor the given distance towards the given point of X and Y.

By default, if the distance to the point is smaller than the given
distance, the actor will only move up to the point but not overshoot it.
If the optional parameter ``overshoot`` is given as True however, the
actor will move past the target point if the given distance is far enough.


.. method:: Actor.move_forward(distance)

Moves the actor forwards along its current angle by the given distance.


.. method:: Actor.move_backward(distance)

Moves the actor backwards in the opposite direction of its current angle
by the given distance.

.. method:: Actor.move_left(distance)

Moves the actor sideways to the left when viewing its angle as forward.

This does not mean the actor moves along the Y-axis, but instead that if
the actor is pointing to the right, then right is forward to the actor and
left from its perspective would be up in the game window.

.. method:: Actor.move_right(distance)

Moves the actor sideways to the right when viewing its angle as forward.

The same applies here. Right is always in relation to where the actor is
pointing.

These function could be used to have actors always move towards the player,
circle around a point in a level, get pushed away from something or many other
options. As a small example, let's have the spaceship follow the mouse around
the game window::

ship = Actor('ship')
mouse_position = (0, 0)

def on_mouse_move(pos):
# To change mouse_position from within a function,
# we have to declare it global here.
global mouse_position
mouse_position = pos

def update():
# To just read the value of the global variable,
# we don't have to do anything else.
ship.move_towards_point(mouse_position, 5)

*Note:* When using ``move_towards_point()`` with ``overshoot=True``, if the
function is called every frame (for example in ``update()``), the actor will
rapidly jump back and forth since the angle to the target point gets inverted
every frame. To prevent this, use ``move_towards_point()`` without ``overshoot``
or make sure it is not called rapidly with ``overshoot``.


.. _transparency:

Transparency
Expand Down
48 changes: 48 additions & 0 deletions src/pgzero/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ def angle(self):

@angle.setter
def angle(self, angle):
# Keeps the angle between 0 and 359 degrees
angle = angle % 360
self._angle = angle
w, h = self._orig_surf.get_size()

Expand Down Expand Up @@ -350,6 +352,52 @@ def angle_to(self, target):
dy = myy - ty # y axis is inverted from mathematical y in Pygame
return degrees(atan2(dy, dx))

def move_towards_angle(self, angle, distance):
"""Move the actor a certain distance towards a certain
angle. Does not change the actors angle property.
All other functions for movement around angles use
this basic function."""
# Modulo of angle is there to prevent invalid angles leading to
# incorrect movement because of wrong radian values messing up
# the calculation.
rad_angle = radians(angle % 360)
move_x = cos(rad_angle) * distance
move_y = -1 * sin(rad_angle) * distance
self.x += move_x
self.y += move_y

def move_towards_point(self, point, distance, overshoot=False):
"""Figure out the angle to the given point and then
move the actor towards it by the given distance."""
angle = self.angle_to(point)
if overshoot:
self.move_towards_angle(angle, distance)
else:
m_distance = min(self.distance_to(point), distance)
self.move_towards_angle(angle, m_distance)

def move_forward(self, distance):
"""Move the actor in the direction it is facing."""
self.move_towards_angle(self._angle, distance)

def move_backward(self, distance):
"""Move the actor in the opposite direction of its
heading."""
angle = (self._angle + 180) % 360
self.move_towards_angle(angle, distance)

def move_left(self, distance):
"""Move the actor left based on its heading. "Strafing"
left."""
angle = (self._angle + 90) % 360
self.move_towards_angle(angle, distance)

def move_right(self, distance):
"""Move the actor right based on its heading. "Strafing"
right."""
angle = (self._angle - 90) % 360
self.move_towards_angle(angle, distance)

def distance_to(self, target):
"""Return the distance from this actor's pos to target, in pixels."""
if isinstance(target, Actor):
Expand Down
76 changes: 76 additions & 0 deletions test/test_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,79 @@ def test_dir_correct(self):
a = Actor("alien")
for attribute in dir(a):
a.__getattr__(attribute)

def test_move_to_angle(self):
"""Ensure moving towards an arbitrary angle works."""
# We set the anchor to topleft for easier math.
a = Actor("alien", anchor=("left", "top"))
# Pythagoras for necessary distance to reach the target point.
distance = (50**2 + 50**2)**0.5
a.move_towards_angle(-45, distance)
# After moving we always have to round to match the int target point.
# In actual games, having the position be floats is no problem.
a.pos = (round(a.x), round(a.y))
self.assertEqual(a.pos, (50, 50))

def test_move_to_point(self):
"""Ensure moving towards a point works."""
a = Actor("alien", anchor=("left", "top"))
position = (50, 50)
distance = ((50**2 + 50**2)**0.5)/2
a.move_towards_point(position, distance)
a.pos = (round(a.x), round(a.y))
self.assertEqual(a.pos, (25, 25))

def test_move_to_point_no_overshoot(self):
"""Ensure moving towards point won't overshoot if distance to target
is smaller than the given distance to move."""
a = Actor("alien", anchor=("left", "top"))
position = (10, 10)
distance = ((50**2 + 50**2)**0.5)/2
a.move_towards_point(position, distance)
a.pos = (round(a.x), round(a.y))
self.assertEqual(a.pos, (10, 10))

def test_move_to_point_with_overshoot(self):
"""Ensure position overshoots correctly if given the parameter."""
a = Actor("alien", anchor=("left", "top"))
position = (10, 10)
distance = ((50**2 + 50**2)**0.5)/2
a.move_towards_point(position, distance, overshoot=True)
a.pos = (round(a.x), round(a.y))
self.assertEqual(a.pos, (25, 25))

def test_move_forward(self):
"""Test whether moving forward by the actor angle works."""
a = Actor("alien", anchor=("left", "top"))
a.angle = -45
distance = (50**2 + 50**2)**0.5
a.move_forward(distance)
a.pos = (round(a.x), round(a.y))
self.assertEqual(a.pos, (50, 50))

def test_move_backward(self):
"""Test whether moving backwards by the actor angle works."""
a = Actor("alien", anchor=("left", "top"))
a.angle = 135
distance = (50**2 + 50**2)**0.5
a.move_backward(distance)
a.pos = (round(a.x), round(a.y))
self.assertEqual(a.pos, (50, 50))

def test_move_left(self):
"""Test whether moving left by the actor angle works."""
a = Actor("alien", anchor=("left", "top"))
a.angle = -135
distance = (50**2 + 50**2)**0.5
a.move_left(distance)
a.pos = (round(a.x), round(a.y))
self.assertEqual(a.pos, (50, 50))

def test_move_right(self):
"""Test whether moving right by the actor angle works."""
a = Actor("alien", anchor=("left", "top"))
a.angle = 45
distance = (50**2 + 50**2)**0.5
a.move_right(distance)
a.pos = (round(a.x), round(a.y))
self.assertEqual(a.pos, (50, 50))